All admin calls use the super-admin GraphQL API with the
x-authorizer-admin-secret header (the --admin-secret value, admin in
these manifests). The Origin header is required — Authorizer's CSRF guard
rejects state-changing requests without it.
kind create cluster --name authorizer-demo --config k8s/kind-config.yaml
# Authorizer server image (built from server main; substitute the next
# published release once it ships)
kind load docker-image authorizer:local --name authorizer-demo
# Worker image (vendor first — go.mod has a local replace, see app/Dockerfile)
cd app && go mod vendor && docker build -t authorizer-worker:local . && cd ..
kind load docker-image authorizer-worker:local --name authorizer-demo
kubectl apply -f k8s/rbac.yaml -f k8s/authorizer.yaml
# Wait for authorizer to be ready before creating the trusted issuer:
kubectl -n authorizer-demo rollout status deploy/authorizerAuthorizer is now reachable on the host at http://localhost:30080.
Projected ServiceAccount tokens are JWTs signed by the cluster:
kubectl get --raw /.well-known/openid-configuration | jq -r .issuer
# kind default: https://kubernetes.default.svc.cluster.local
kubectl get --raw /openid/v1/jwks > jwks.jsonSSRF constraint (important): Authorizer fetches trusted-issuer JWKS with an
SSRF-hardened client that rejects private, loopback, and link-local addresses.
A kind cluster's own JWKS endpoint lives on a private ClusterIP, so Authorizer
cannot fetch it directly. Host jwks.json at a publicly-routable URL
(only the public keys — nothing sensitive), e.g.:
# any public static host works; ngrok in front of a local file server:
python3 -m http.server 9000 & # serves ./jwks.json
ngrok http 9000 # note the https://….ngrok…/ URL
JWKS_URL="https://<your-tunnel>/jwks.json"On managed clusters (EKS/GKE/AKS) the issuer URL is already public — use
key_source_type: "oidc_discovery" there and skip this step.
curl -s http://localhost:30080/graphql \
-H 'Content-Type: application/json' \
-H 'Origin: http://localhost:30080' \
-H 'x-authorizer-admin-secret: admin' \
-d '{"query":"mutation { _create_client(params: { name: \"k8s-worker\", allowed_scopes: [\"read:demo\"] }) { client { id client_id } client_secret } }"}'Note the returned client.id (internal id — this is what the trusted issuer
binds to). The client_secret is returned once; for this secretless flow you
can simply discard it.
issuer_url must equal the token's iss exactly; expected_aud must equal
the audience projected in k8s/worker.yaml; allowed_subjects is an exact
allow-list (empty = deny-all).
curl -s http://localhost:30080/graphql \
-H 'Content-Type: application/json' \
-H 'Origin: http://localhost:30080' \
-H 'x-authorizer-admin-secret: admin' \
-d '{"query":"mutation { _add_trusted_issuer(params: { service_account_id: \"<client.id from step 3>\", name: \"kind demo worker\", issuer_url: \"https://kubernetes.default.svc.cluster.local\", key_source_type: \"static_jwks_url\", jwks_url: \"<JWKS_URL from step 2>\", expected_aud: \"http://authorizer.authorizer-demo.svc:8080\", allowed_subjects: \"system:serviceaccount:authorizer-demo:worker\", issuer_type: \"kubernetes_sa\" }) { id issuer_url is_active } }"}'kubectl apply -f k8s/worker.yaml
kubectl -n authorizer-demo logs -f deploy/worker
# expect: "got access token (...)" then "sample endpoint 200: {...}"Offline JWKS validation proves the assertion was signed by the cluster, not
that the pod/ServiceAccount still exists. Authorizer implements online
TokenReview validation (authentication.k8s.io/v1) per trust row via two
fields on the trusted-issuer record: enable_token_review and
kubernetes_api_server_url.
Honest status, verified against server source:
- Runtime: implemented. When
enable_token_reviewis set on the row, Authorizer POSTs the assertion to<kubernetes_api_server_url>/apis/authentication.k8s.io/v1/tokenreviewsafter all offline checks and rejects fail-closed unless the apiserver reportsauthenticated: true. It authenticates that call with its own in-cluster ServiceAccount token — which is whyk8s/rbac.yamlbinds theauthorizerServiceAccount tosystem:auth-delegator. - Admin API: NOT settable yet. Neither
_add_trusted_issuernor_update_trusted_issueraccepts these two fields, and they are not returned by the queries. Today they can only be set by writing the columns directly in the database. This example ships the RBAC so everything is in place the moment the admin API exposes them. - SSRF limitation: the TokenReview call goes through the same
SSRF-hardened client, so only a publicly-routable apiserver endpoint works
(managed clusters' public API endpoints).
https://kubernetes.default.svcis a private ClusterIP and is rejected by design — a kind cluster cannot use TokenReview at all.