Dashboard authentication¶
A shared dashboard signs people in before it shows anything. Every view then follows that person's Kubernetes RBAC and every action runs as them:
- Reads come from a shared cache, filtered by your RBAC. The dashboard keeps one copy of the delivery Stages and Operations, read and watched with its own ServiceAccount. Before it answers you from that copy, it asks the API server (SubjectAccessReview) whether you may make the same list or get there, so a namespace your RBAC denies stays out of every view, and a list across every namespace needs your cluster-wide permission.
- Actions use your token. Propose, approve, cancel, roll back and unfreeze go to the API server with your own token, so RBAC and admission decide and the audit log names you. The reads an action is built on use your token too. The dashboard offers an action only where your RBAC allows it, with the same 10-second review as the reads.
- The fleet views show only the namespaces you may read (who sees what). A service known only from a registry belongs to no namespace, so only people who may list Pactos in every namespace see it, and counts and totals leave it out for everyone else.
The dashboard's ServiceAccount stays read-only: it may get, list and watch Stages and Operations and ask the API server what a person may read. There is no impersonation.
How it works¶
- A person opens the dashboard and sees only its sign-in screen. Every API
endpoint and
/metricsanswer401with the codeLoginRequireduntil they sign in; the page itself,/health,/api/sessionand/auth/*stay open. - They sign in with your identity provider (OpenID Connect, authorization code with PKCE) or, where you allow it, paste a Kubernetes bearer token.
- The dashboard keeps the tokens in a server-side session behind an
HttpOnly,Securecookie. Browser JavaScript never sees a token. - Each request uses the session's token. The API server says who it belongs
to and RBAC decides, exactly as for
kubectl. The dashboard keeps each answer about the delivery objects you may read for 10 seconds and each answer about the namespaces the fleet views show for a minute, so a removed RoleBinding hides its namespace within that time. Until the shared cache has loaded, and for a moment after you change something, the dashboard reads with your token instead.
On every page, the person button in the header opens who you are, your groups
and Sign out.
Approving a held change needs an identity provider sign-in, and a recent one
where delivery asks for it
(a recent sign-in to decide).
The delivery views read their own settings under dashboard.delivery
(the dashboard's delivery settings).
A dashboard on your own machine (pacto dashboard on loopback, with no
sign-in configured) acts as your kubeconfig user and shows no sign-in screen.
Configure the chart¶
dashboard:
auth:
oidc:
issuerURL: https://idp.example.com
clientID: pacto-dashboard
clientSecretRef: {name: dashboard-oidc, key: client-secret}
redirectURL: https://pacto.example.com/auth/callback
extraScopes: [offline_access] # what your provider needs beyond openid, profile and groups
displayName: Keycloak # the button says "Sign in with Keycloak"; empty: "Sign in with OIDC"
# The claim mapping: the same values as the API server's claimMappings.
usernameClaim: sub # empty: sub
usernamePrefix: "" # empty: the issuer URL and #, none for email; "-" for none
groupsClaim: groups # empty: groups
groupsPrefix: ""
session:
idleTimeout: 1h # empty: 1h
maxAge: 12h # empty: 12h
tokenLogin:
enabled: false
For an evaluation cluster, the chart can bundle Dex and wire it for you (quickstart).
The sign-in screen's button says Sign in with OIDC. Set displayName to
the name people know the provider by and it says Sign in with that name
instead.
Register https://<dashboard>/auth/callback as the client's redirect URL and
terminate TLS in front of the dashboard. pacto dashboard takes the same
settings as --oidc-*, --session-* and --token-login flags
(CLI reference), with the client secret in
PACTO_DASHBOARD_OIDC_CLIENT_SECRET. A remote pacto mcp with
--auth-issuer takes the same claim mapping flags
(who sees what).
Reach the issuer¶
The dashboard reads the issuer's discovery document, keys and tokens itself. Two settings cover an issuer it cannot reach the way browsers do:
dashboard:
auth:
oidc:
issuerURL: https://idp.example.com
# Discovery, tokens and keys are read here; browsers still go to issuerURL.
internalIssuerURL: http://dex.dex.svc:5556
# PEM certificates of the CAs that sign the issuer's certificate.
caBundle: {configMap: corporate-ca, key: ca.crt} # or {secret: corporate-ca}
caBundlenames a ConfigMap or a Secret in the operator namespace whose key holds a PEM bundle. The dashboard trusts those CAs on top of the image's public roots, for an issuer whose certificate a corporate or self-signed CA signs.internalIssuerURLsends every request for the issuer's scheme and host to that address, with the issuer's path replaced by the internal URL's. The discovery document and every ID token must still nameissuerURL, so the issuer must be configured with its public URL.
pacto dashboard takes them as --oidc-ca-file and
--oidc-internal-issuer-url.
Match the API server's claim mapping¶
The API server decides who a token is. At sign-in and on every renewal the dashboard asks it (SelfSubjectReview) with the person's ID token, and every view uses the answer. The claim mapping names the person only while the API server does not accept the ID token, for example before it trusts the issuer, so it must equal the API server's. With the defaults above, the API server's structured authentication reads:
apiVersion: apiserver.config.k8s.io/v1
kind: AuthenticationConfiguration
jwt:
- issuer: {url: https://idp.example.com, audiences: [pacto-dashboard]}
claimMappings:
username: {claim: sub, prefix: "https://idp.example.com#"}
groups: {claim: groups, prefix: ""}
Then a mismatch shows as the wrong fleet: the views ask about a user the API
server never sees. To check, sign in and compare the header with kubectl auth whoami
run with a token from the same issuer. The dashboard drops any group starting
with system: from a token, as the API server does.
Sessions¶
The dashboard renews the ID token with the refresh token before it expires, so
a session lasts until idleTimeout passes without a request or maxAge after
sign-in. A provider that issues refresh tokens only for offline_access needs
it in extraScopes; without a refresh token the session ends when the ID token
expires. Renewal keeps the original login time, which is what
a recent sign-in to decide
measures when delivery asks for one (delivery.promoter.authFreshness).
Sign out ends the session, revokes the refresh token where the provider
supports revocation and, where the provider publishes an
end_session_endpoint, sends the browser there to end the provider's session
too. Sessions live in the memory of one dashboard process. The operator runs
the dashboard as one replica, and a restart signs everyone out. A
pacto dashboard you deploy yourself with sign-in needs one replica too, or a
load balancer with session affinity (for example sessionAffinity: ClientIP
on its Service) so each person keeps reaching the replica that holds their
session.
Token sign-in¶
Set tokenLogin.enabled: true for a cluster whose API server trusts no
identity provider you can use. The sign-in screen then takes any bearer token
the API server accepts, such as a ServiceAccount token from
kubectl create token. The dashboard checks it with a SelfSubjectReview,
keeps it in the server-side session and checks it again every five minutes, so
a token that expires or is revoked ends its session. A token session reads and
acts with that token's RBAC and never decides on a held change: it proves no
recent sign-in. Token sign-in works with or without oidc.
To get a token:
- For a ServiceAccount, run
kubectl create token <service-account> -n <namespace>. Add--durationfor a token that lasts longer than the default hour, up to what the API server allows. - For yourself with a static token in your kubeconfig, run
kubectl config view --raw --minify -o jsonpath='{.users[0].user.token}'. - For yourself on EKS, run
aws eks get-token --cluster-name <cluster>and take itsstatus.tokenfield. - For yourself on GKE, run
gcloud auth print-access-token.
Cloud provider tokens are short-lived, and the session ends when its token expires.
Identity providers¶
| Provider | issuerURL |
Claim mapping | Notes |
|---|---|---|---|
| Dex | https://dex.example.com |
defaults | Add offline_access for renewal. For approvals add audience:server:client_id:pacto-cli (why). |
| Keycloak | https://keycloak.example.com/realms/<realm> |
usernameClaim: preferred_username |
Add a Group Membership mapper named groups with full group path off. |
| Microsoft Entra ID | https://login.microsoftonline.com/<tenant-id>/v2.0 |
usernameClaim: oid |
Add the groups optional claim; groups arrive as object IDs, so bind RBAC to those. Add offline_access for renewal. |
| Okta | https://<org>.okta.com/oauth2/<server-id> |
defaults | Add a groups claim to the authorization server and offline_access for renewal. |
https://accounts.google.com |
usernameClaim: email |
The ID token carries no groups, so bind RBAC to users. Sessions end when the ID token expires, after about an hour. |
Use oid with Entra ID because its sub differs per application.
Managed clusters¶
The API server must trust the same issuer with the same mapping.
- EKS: associate an OIDC identity provider with the cluster
(
aws eks associate-identity-provider-config) and give it the sameusernameClaim,usernamePrefix,groupsClaimandgroupsPrefix. - GKE: configure your provider in Identity Service for GKE and match its
userClaim,userPrefix,groupsClaimandgroupPrefix. - AKS: managed Entra ID integration accepts tokens for its own server application. To sign in with the dashboard's client, use structured authentication where the cluster offers it, or token sign-in.
Approvals need more from the API server than sign-in does; the approval table lists which clusters can provide it.