Warning
Do not keep ate-client as the bootstrap owner outside development.
ate-setup makes the shared ate-client ServiceAccount the default
bootstrap owner so the demos, benchmarks and e2e tests work. Anyone who can
mint a token for it, including every kubectl ate user who doesn't pass
--token-file, is then a global owner. For a shared or production install,
set bootstrap owners to your operators' own principals.
ate-api-server checks every API call against an
OpenFGA model before running it. OpenFGA is embedded in
ate-api-server and stores its data in the same PostgreSQL database. For how
callers authenticate, see authentication.md.
- Authentication turns the credential into a principal ID, or rejects it
with
Unauthenticated. - Authorization allows the call if the principal is a bootstrap owner,
holds a role from an access policy that permits it, or is a Substrate
component with the system role it needs. Otherwise, or if the RPC has no
rule, it returns
PermissionDenied.
These are ate-setup settings, so the environment variable is read by
ate-setup. It passes them to ate-api-server as the
--experimental-enable-authz and --authz-bootstrap-owners flags on its
container; an install that doesn't use ate-setup sets those flags in its
manifest. A change rolls ate-api-server.
| Setting | Environment variable | Example |
|---|---|---|
ateapi.authz.enabled |
ATE_API_EXPERIMENTAL_ENABLE_AUTHZ |
true (the default) |
ateapi.authz.bootstrapOwners |
ATE_API_AUTHZ_BOOTSTRAP_OWNERS |
alice@example.com,bob@example.com |
If you don't set ateapi.authz.bootstrapOwners, ate-setup uses the shared
ate-client ServiceAccount (system:serviceaccount:<namespace>:ate-client),
so the demos, benchmarks and e2e tests work. See the warning at the top of
this page.
ATE_API_EXPERIMENTAL_ENABLE_AUTHZ=false hack/install-ate.sh --deploy-ate-system # turn it offWith authorization off, access policy RPCs are still checked, so nobody can grant themselves access while it is off and keep it once it is on. Bootstrap owners still apply, so they can set up policies before turning it on.
A principal is the ID authentication produces. Access policies name it as
user:<id>; bootstrap owners list the bare <id>.
| Caller | <id> |
|---|---|
| Kubernetes ServiceAccount token | system:serviceaccount:<namespace>:<name> |
| Other JWT provider, such as Google | The token's sub (for Google, a numeric account ID, not the email) |
| Pod certificate (mTLS) | Its SPIFFE ID, e.g. spiffe://cluster.local/ns/benchmarking/sa/default |
A denied call names the principal in the error with : percent-encoded
(user:system%3Aserviceaccount%3A...); decode it before using it.
Bootstrap owners always hold owner on the global scope, whatever the access
policies say. They write an install's first access policies and recover access
if those break.
- They come from configuration, not the API.
ate-api-serverreads them at startup and exits if authorization is on and the list is empty. Restarting it with a new list takes effect immediately. - Any principal
ate-api-servercan authenticate can be one. ate-setupwarns wheneverate-clientis one.- Keep the list short. Grant day-to-day admins
ownerin the global access policy instead, where it can be revoked through the API.
For example, with a JWT provider whose tokens carry the user's email as sub:
ATE_API_AUTHZ_BOOTSTRAP_OWNERS=alice@example.com,bob@example.com hack/install-ate.sh --deploy-ate-systemTo make your Google account a bootstrap owner, use its numeric sub:
gcloud auth print-identity-token | cut -d. -f2 | base64 -d 2>/dev/null | jq -r .sub # your sub
ATE_API_AUTHZ_BOOTSTRAP_OWNERS=<sub>,system:serviceaccount:ate-system:ate-client \
hack/install-ate.sh --deploy-ate-system # keep ate-client for demos and benchmarksThen add the Google provider to the ate-api-authentication ConfigMap and
restart ate-api-server
(authentication.md), and act as
yourself:
gcloud auth print-identity-token > me.token
kubectl ate --token-file me.token get atespaceskubectl ate sends the token from --token-file (- for stdin). Without
one, it uses your kubeconfig to mint a token for ate-client (or
ATE_CLIENT_SERVICE_ACCOUNT) and warns that it is acting as a shared
identity. It still needs Kubernetes rights to port-forward to ate-api-server
unless you pass --endpoint.
ate-setup calls the API only to deploy and delete demos and benchmarks, as
ate-client. If you leave ate-client out of the bootstrap owners, grant it
access in a policy or those steps are denied.
Roles are granted on the global scope, which covers every atespace, or on one atespace.
| Permission | Global owner | Global viewer | Atespace owner | Atespace editor | Atespace viewer |
|---|---|---|---|---|---|
| Create atespaces; list all atespaces | ✅ | list only | |||
| Get the atespace | ✅ | ✅ | ✅ | ✅ | ✅ |
| Delete the atespace | ✅ | ✅ | |||
| Get access policies | ✅ | ✅ | ✅ (its own) | ✅ (its own) | ✅ (its own) |
| Create, update, delete access policies | ✅ | ✅ (its own) | |||
| Get and list actors, actor templates, tags, egress policies | ✅ | ✅ | ✅ | ✅ | ✅ |
| Create, update, delete actors and actor templates | ✅ | ✅ | ✅ | ||
| Pause, resume, suspend, revert actors | ✅ | ✅ | ✅ | ||
| Create, update, delete egress policies | ✅ | ✅ | ✅ | ||
| Create, publish, delete tags | ✅ | ✅ | ✅ | ||
Create an actor from a tag (sourceTag) |
✅ | ✅ | ✅ | ||
| Workers: get, list, list assignments | ✅ | ✅ | |||
| Workers: create, update, delete, drain | ✅ |
Substrate's components hold roles that access policies cannot grant, and only
through their pod certificates. ate-api-server's --controller-spiffe-id,
--egress-gateway-spiffe-id and --ingress-router-spiffe-id flags bind them,
defaulting to the install's; atelet's comes from --atelet-service-account.
| Role | Held by | Allows |
|---|---|---|
controller |
ate-controller |
Create, get, list, update, delete and drain workers |
egress_gateway |
atenet-egress |
Get actors and their egress policies; list actors across all atespaces; mint actor JWTs and certificates |
ingress_router |
atenet-router |
Resume actors; list actors across all atespaces |
atelet |
atelet |
Get actors' egress policies, for the worker that enforces them |
Only egress_gateway can mint actor credentials: not even a global owner can,
since a credential lets its holder act as the actor.
A gRPC interceptor makes every check before the handler runs
(registry.go,
model.fga). When an RPC lists
several checks, all must pass.
Roles inherit: an owner is also an editor and a viewer, an editor is also a viewer, and a global role holds the same role in every atespace. So "atespace editor" below also means atespace owner, global owner, and so on.
| RPC | Permission, on | Allowed for |
|---|---|---|
CreateAtespace |
can_create_atespace, global |
Global owner |
ListAtespaces |
can_list_atespaces, global |
Global viewer |
GetAtespace |
can_get, the atespace |
Atespace viewer |
DeleteAtespace |
can_delete, the atespace |
Atespace owner |
GetGlobalAccessPolicy |
can_get_access_policy, global |
Global viewer |
CreateGlobalAccessPolicy, UpdateGlobalAccessPolicy |
can_create_access_policy / can_update_access_policy, global |
Global owner |
GetAtespaceAccessPolicy |
can_get_access_policy, the atespace |
Atespace viewer |
CreateAtespaceAccessPolicy, UpdateAtespaceAccessPolicy, DeleteAtespaceAccessPolicy |
can_create_access_policy / can_update_access_policy / can_delete_access_policy, the atespace |
Atespace owner |
The access policy RPCs are checked even with authorization off.
| RPC | Permission, on | Allowed for |
|---|---|---|
CreateActorTemplate |
can_create_actor_template, metadata.atespace |
Atespace editor |
GetActorTemplate |
can_get, the template |
Viewer of its atespace |
DeleteActorTemplate |
can_delete, the template |
Editor of its atespace |
ListActorTemplates |
can_list_actor_templates, the atespace, or global if none is given |
Atespace viewer; global viewer for all atespaces |
CreateActor |
1. can_create_actor, the actor's atespace2. can_use_template, the actor template3. can_use_tag, the sourceTag, if one is given |
1. Atespace editor 2. Viewer of the template's atespace, which may be another one 3. Editor of the tag's atespace. Golden tags skip this check: the handler only accepts one built from the actor's own template. |
UpdateActor |
1. can_update, the actor2. can_use_template, the template in the request |
1. Editor of its atespace 2. Viewer of the template's atespace, so an actor can't be switched to a template its editor can't use |
GetActor |
can_get, the actor |
Viewer of its atespace; atenet-egress |
DeleteActor |
can_delete, the actor |
Editor of its atespace |
ListActors |
can_list_actors, the atespace, or global if none is given |
Atespace viewer; for all atespaces, global viewer, atenet-router or atenet-egress |
PauseActor, SuspendActor, RevertActor |
can_pause / can_suspend / can_revert, the actor |
Editor of its atespace |
ResumeActor |
can_resume, the actor |
Editor of its atespace; atenet-router |
GetActorEgressPolicy |
can_get_egress_policy, the actor |
Viewer of its atespace; atenet-egress; atelet |
CreateActorEgressPolicy, UpdateActorEgressPolicy, DeleteActorEgressPolicy |
can_create_egress_policy / can_update_egress_policy / can_delete_egress_policy, the actor |
Editor of its atespace |
MintActorJwt, MintActorCertificate |
can_mint_actor_jwt / can_mint_actor_certificate, the actor |
atenet-egress only |
| RPC | Permission, on | Allowed for |
|---|---|---|
CreateTag |
can_create_tag, the source actor's atespace, where the tag is stored |
Atespace editor |
GetTag |
can_get, the tag |
Viewer of its atespace |
UpdateTag (including publishing) |
can_update, the tag |
Editor of its atespace |
DeleteTag |
can_delete, the tag |
Editor of its atespace |
ListTags |
can_list_tags, the atespace, or global if none is given |
Atespace viewer; global viewer for all atespaces |
Workers have no atespace; their permissions come from the global scope.
| RPC | Permission, on | Allowed for |
|---|---|---|
CreateWorker |
can_create_worker, global |
Global owner; ate-controller |
ListWorkers |
can_list_workers, global |
Global viewer; ate-controller |
GetWorker |
can_get, the worker |
Global viewer; ate-controller |
UpdateWorker, MarkWorkerAvailable, MarkWorkerUnavailable |
can_update, the worker |
Global owner; ate-controller |
DeleteWorker |
can_delete, the worker |
Global owner; ate-controller |
DrainWorker |
can_drain, the worker |
Global owner; ate-controller |
ListWorkerActorAssignments |
can_list_actor_assignments, the worker |
Global viewer |
The interceptor skips these; each handler requires atelet's pod
certificate.
| RPC | The caller must be |
|---|---|
RegisterWorker |
atelet on the worker's node |
RequestActorSuspend |
atelet on the node of the worker hosting the actor |
MintWorkerActorJwt |
atelet on the node hosting the actor |
MintWorkerActorCertificate |
atelet on the node of the worker the actor is assigned to |
ReportActorSnapshotState has a rule but is not served yet.
With authorization on:
- An RPC with no rule is denied, and a test fails the build if a served RPC has none.
- An unauthenticated caller gets
Unauthenticatedbefore its request is read. - A missing or invalid resource name returns
InvalidArgument; no check is skipped.
Each scope has one access policy, named default, managed with kubectl ate
using --global or -a <atespace>:
# policy.yaml
metadata:
name: default
bindings:
- role: editor
members:
- user:system:serviceaccount:team-a:alice
- role: viewer
members:
- user:system:serviceaccount:team-a:bobkubectl ate --token-file me.token create access-policy -a team-a -f policy.yaml
kubectl ate --token-file me.token get access-policy -a team-a -o yaml > current.yaml
kubectl ate --token-file me.token update access-policy -a team-a -f current.yaml
kubectl ate --token-file me.token delete access-policy -a team-a- Global policies accept
ownerandviewer; atespace policies also accepteditor. updatereplaces the whole policy and needs theuidandversionfromget -o yaml. It fails if the policy changed, or was deleted and recreated, since.- The global policy can't be deleted; update it with no bindings instead.
- Deleting an atespace deletes its policy, so a recreated atespace starts with none.
- Changes take effect on the next call.
If no principal anyone can authenticate as holds owner (for example, a
Google account listed by email instead of its numeric sub), anyone who can
redeploy ate-api-server can recover: redeploy with a bootstrap owner they
can use, fix the policies, then redeploy with the intended owners.
ATE_API_AUTHZ_BOOTSTRAP_OWNERS=alice@example.com hack/install-ate.sh --deploy-ate-systemFor a shared cluster, beyond replacing the default bootstrap owner:
-
Give people their own identities in access policies, and have them pass
--token-filewith a token from a trusted JWT provider or their own ServiceAccount. To let a user mint tokens only for their own ServiceAccount:apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: {name: mint-alice-token, namespace: team-a} rules: - apiGroups: [""] resources: ["serviceaccounts/token"] resourceNames: ["alice"] verbs: ["create"]
kubectl create token -n team-a alice --audience api.ate-system.svc > alice.token kubectl ate --token-file alice.token get actors -a team-a -
Limit who can act as a ServiceAccount bootstrap owner. Anyone who can create
serviceaccounts/token, pods or Secrets in its namespace, or change RBAC there, can get its token. Akubernetes.io/service-account-tokenSecret gets one that never expires. Check who can, and alert on the audit events:kubectl auth can-i create serviceaccounts/ate-client --subresource=token -n ate-system --as alice@example.com
- Signing in.
kubectl atehas no login flow or credential plugin; people obtain tokens themselves. - JWT providers at install time.
ate-setupconfigures only the cluster's ServiceAccount issuer; add others to the ConfigMap afterwards. - Groups. Policy members are individual principals.
- Tags can copy an actor's data. A tag is a durable copy of an actor's memory and disk, including secrets, and outlives the actor. Any atespace editor can tag, publish and clone any actor in the atespace.
- Principals are not qualified by issuer. If
ate-api-servertrusts more than one JWT provider, any of them that can issue a token whosesubis another principal's ID, a bootstrap owner's included, acts as that principal. System roles are unaffected: they need the component's pod certificate. - The atespace is the only boundary. There are no per-actor or per-template grants.
- Any atelet can read any actor's egress policy. Every node's atelet
shares one identity, so the
ateletrole isn't limited to actors on its node (#1564).