Beta: this feature and the policy input contract described below are still
evolving and are likely to change. Expect to revisit your policies as the
feature matures.
In the Qovery Console, this feature is labelled Policy API Token (Beta),
in the Token API section of your organization settings.
Why use one
A role answers “what kind of thing may this token do, across a scope”. A policy answers “may this exact request proceed”. That is what makes it a fit for an agent: you can hand out a token that reads everything in one environment, deploys that environment, may update one service, and can never delete anything - a shape no role expresses.How it works
What Open Policy Agent does
Open Policy Agent (OPA) is an open source, general-purpose policy engine and a CNCF graduated project. Its model is deliberately narrow: it knows nothing about Qovery and holds no permission model of its own. You give it two things - a policy, and a JSON document called the input - and it answers a query against them. That is all it does. That separation is the point. The rules deciding what your agent may do live in the policy rather than in Qovery’s permission code, so you can express a constraint Qovery never anticipated without Qovery having to invent a role for it. Qovery runs OPA as a sidecar next to the API, reachable only over the pod’s loopback interface. Your policy and the requests it is evaluated against never leave Qovery’s infrastructure, and a decision costs an in-pod round trip rather than a call out to a network service.How Qovery evaluates a request
The policy is evaluated at authentication time, before the request reaches the endpoint it targets. That is also why the policy sees the request’s raw path rather than a route template - the route has not been matched yet.- The token is recognised by its
sk-qov-01prefix in theAuthorizationheader, which is what tells it apart from a regular API token sent under the sameTokenscheme. - The token is looked up. Only a hash of the token value is stored, so the presented value is hashed and matched against it. An unknown or expired token stops here.
- The target is resolved. Qovery takes the IDs appearing in the request path and asks its own database what they are - a service, an environment, a project - and what that resource’s ancestry is. This is what fills
qovery_metadata, and it is why a policy can talk about environments while the request only names a service. - The input document is built from the request and that resolved metadata. Its exact shape is documented below.
- The policy is loaded. The first time a pod handles a given token, it uploads that token’s policy to its OPA sidecar as a module, under a package unique to the token. Per-token packages are what stop one token’s rules from ever contributing to another token’s decision, and they are the reason your policy must not declare a
packageof its own. The module is compiled once and reused for subsequent requests. - The policy is evaluated against the input, and OPA returns the value of
allow:true,false, or undefined when no rule produced a value. - The decision is applied.
truelets the request continue to the endpoint. Anything else - including undefined, a non-boolean value, or an OPA that could not be reached - answers401 Unauthorized.
Rego, the policy language
Policies are written in rego, OPA’s language. It is declarative and derives from Datalog, extended to query nested JSON documents - which is exactly what aninput document is. There are no statements to sequence and no control flow to trace: you declare the conditions under which something holds, and OPA searches for a way to satisfy them.
Five ideas carry almost every Qovery policy.
A rule has a head and a body, and the body is an AND. Every expression in the body must hold for the rule to produce its value.
allow twice is not a conflict, it adds an alternative. This is how a policy grants several unrelated things without collapsing into one tangled condition:
input.request.body.key on a request that carried no body is undefined, so the rule using it does not fire, so the request is denied. Nothing throws, and fail-closed comes out as the natural default rather than something bolted on.
default supplies a value when nothing fires. Without it, a request matching no rule leaves allow undefined; with it, the answer is explicitly false.
:= assigns, == compares, in tests membership. Membership needs no loop, which keeps a list of allowed verbs readable:
startswith, endswith, count, regex.match, sprintf and many more - listed in the policy reference.
What this means in practice
- The policy is the only thing constraining the token. Internally a policy token is granted organization-admin access, and the policy is what narrows it. A policy whose
allowis unconditionallytruetherefore grants full organization-admin access. - It fails closed. Anything other than an explicit
truedenies the request: no matching rule, a non-booleanallow, a request path that Qovery cannot resolve, or a policy engine that cannot be reached. - Only an organization Owner or Admin can create one. Creating a policy token is equivalent to handing out organization-admin access, so it is restricted to roles that already hold it. Custom roles cannot create one.
- Revocation is immediate. The token is looked up on every request, with no caching.
- Actions are attributed in the audit log as
policy:<token-id>:<token-name>, so an agent’s activity is distinguishable from a regular API token’s.
What the policy sees
Your policy is evaluated against thisinput document:
The public API path is internally prefixed with
api, so
POST https://api.qovery.com/environment/<id>/service/deploy reaches your
policy as ["api", "environment", "<id>", "service", "deploy"]. Include that
first segment when you match on a path.- The
qovery_metadatakeys are always present, set tonullwhen nothing of that kind was resolved. Testinput.qovery_metadata.service_id == null, not the absence of the key. - Qovery resolves one target per request, preferring a service over an environment over a project. For
/api/environment/<env>/application/<app>, the target is the application:environment_id,project_idandcluster_idare then read from that service’s ancestry rather than from the path. So a rule onenvironment_idstill holds for a request that only names the service. - IDs belonging to another organization are ignored, as if they were not in the path.
- A request whose path cannot be resolved is denied, rather than shown with
nullfields. A rule phrased asservice_id != "<id>"cannot be widened by a failed lookup. - Only JSON bodies are forwarded. A request with a non-JSON body, a malformed JSON body, or a body larger than 1 MiB is denied outright rather than presented to the policy with
bodyset tonull. Inside a policy,body == nulltherefore always means “there was no body”. - Query parameters, headers and the client IP are deliberately not exposed.
Writing the policy
- The policy must define an
allowrule, and the decision must be the booleantrue. Anything else denies. - Start with
default allow := false. Without it, a request that matches no rule leavesallowundefined, which Qovery also treats as a denial - the default just makes the intent explicit. - Maximum policy size: 65,536 characters.
- The policy is compiled when you create the token, so a syntax error comes back immediately as a
400with the compiler diagnostics. - The engine is OPA 1.19, so rego v1 syntax is available directly:
if,inandcontainsneed noimportstatement.
Starter policy
This is the policy prefilled in the Console when you create a token. It grants read-only access to one environment, write access to one service in it (but never a deletion), and the right to deploy that environment.Policy recipes
Read-only access to one environment
Read-only access to one environment
Every
GET and HEAD on the environment, its services and their sub-resources. Nothing else.Deploy, and nothing else
Deploy, and nothing else
A token for a release pipeline: it can trigger deployments and cannot read or change anything else. Matching on the exact path is what keeps it that narrow.
Change one service, but never delete it
Change one service, but never delete it
Full read and write on a single service, with deletion excluded whatever the endpoint.
Constrain the request body
Constrain the request body
Because the parsed JSON body is part of the input, a rule can constrain the content of a change and not just its target. Here the agent may create environment variables on one application, but only those named A request with no body leaves
FEATURE_*.input.request.body.key undefined, so this rule denies it.Create an API Policy Token
1
Open the Token API section
Go to your organization settings and open the Token API section, the same
page as the API tokens. The
Policy API Token (Beta) section sits below the API token list.
2
Click Add new
Press the Add new button of the Policy API Token section.
3
Configure the token
Provide:
- Token name: a descriptive name, unique within the organization
- Description: what the token is for
- Policy (rego): your policy, prefilled with the starter policy above
4
Store the token value
The token is displayed once, in the form
sk-qov-01-.... Copy it before
closing the modal.Use the token
An API Policy Token uses the sameAuthorization header scheme as a regular API token:
401 Unauthorized.
Inspect and revoke
In the Policy API Token list, each token offers:- Inspect policy (scroll icon): shows the policy attached to the token, read-only. A policy can always be reviewed after creation - only the token value is write-once.
- Delete (trash icon): revokes the token. Confirm the deletion, and the token stops working immediately.
Manage tokens via the API
Creating a token, with the policy passed as a string in
opa_policy:
token field. This is the only response that ever carries it.
The list endpoint returns each token with its opa_policy, never the token value. The create endpoint also accepts an optional expires_at (RFC 3339 date-time), after which the token stops authenticating; the Console form does not expose it yet, so set an expiration through the API if you need one.
Errors to expect when creating a token:
Test a policy before attaching it
A denied request tells you only that it was denied, so it is worth evaluating a policy locally first. Install OPA 1.19 - the same version Qovery runs - then:1
Save the policy with a temporary package line
Qovery adds the
package declaration for you, so add one locally and remove
it before submitting.2
Save the request you want to test
Write an
input.json following the input document
shown above.3
Evaluate
true means the request would be allowed. false, or an empty result, means
it would be denied.Beta limitations
Related
API Token
Role-based tokens for CI/CD, Terraform and scripts.
Securing AI Agent Access
The full picture on giving an AI agent a safe footprint on your infrastructure.
Members & RBAC
The roles that back regular API tokens.