Skip to content

What does sluis trust?

An installation has two trust anchors, and a service accepts those two and no third. When a design and this page disagree, this page wins.

Two anchors

Anchor Root of trust Proves
The cluster The API server checks a ServiceAccount token bound to an audience. Recovery only: somebody who can mint a token for the recovery account in this cluster.
The issuer sluis's signing key, published as a key set. An identity the policy resolved to internal groups: people, CI, workloads, from any cluster or cloud account.

The issuer stands on the cluster: its workload verifier turns a ServiceAccount token into an issuer token. It checks that token against the key set its own cluster publishes, so one issuer serves many clusters and holds access to none.

Choosing the anchor

Scope decides, not preference.

A caller is It presents
A workload in the same cluster as the service Its ServiceAccount token.
A workload in another cluster An issuer token.
A person An issuer token from a sign-in against a corporate directory.
A CI job An issuer token from exchanging the platform's identity token.
A laptop An issuer token from sluisctl trading its own sign-in.
The break-glass operator A ServiceAccount token minted by hand, or the recovery password.

Of the issuer's own tokens, only the access token of a sign_in_exchange client's sign-in, presented by that client, is a proof. An ID token never is.

A service for local workloads needs only the cluster anchor, and one for people only the issuer. A service for both keeps the proofs apart by verifier and never admits an operator call on a workload's proof alone.

One vocabulary

Whichever anchor proved the caller, a service acts on one thing: a list of internal group names. The issuer puts them in the flat groups claim. A service_account matcher gives a ServiceAccount the same names. Every relying party binds on those strings and none re-maps them.

The claim shape is groups only. Kubernetes reads a flat string array, ArgoCD reads groups, and AWS trust policies read aud. A fact about where a grant came from travels inside the string, for example C0north:sluis:viewer. The claims fragment table covers a claim that is not a group, such as cloud session tags.

Naming

Every grant is <scope>:<thing>:<role>: three lowercase segments joined by :.

Segment Is Examples
scope An environment, a workspace id or all. Never a domain. dev, prod, C0north, all
thing What the role is on. k8s, shop, argocd
role A role from that thing's own ladder. admin, viewer, deployer, operator

Examples: prod:k8s:admin, prod:shop:deployer, all:sluis:operator. A role scoped to a workspace id also gates the GitHub organisations and Slack workspaces connected under that directory.

Two-segment names are not grants. rung:<name> carries a session lifetime and emp:<slug> is a person. A cluster-scoped consumer binds <env>:k8s:<role> by default and owns a thing of its own only when its role ladder diverges.

Identity claims travel beside groups and are not authorization. A relying party shows them and never reads them in policy.

sub  email  name  given_name  family_name  preferred_username  sid  auth_time

A person's sub is their email. A ServiceAccount's is <cluster>:k8s:<namespace>:<name>, so the same account on two clusters is two subjects.

Recovery is the root, not a back door

Break-glass is a ServiceAccount token minted with kubectl create token for an account the chart creates bound to nobody. When the directory is broken, the issuer is unavailable and only the cluster is still trusted. Authorization is cluster RBAC: who may mint that token.

It is the only human path that bypasses the issuer. On Lambda a password takes its place. See recovery.

Consoles and APIs

A console lives on the issuer anchor only. The gateway holds the browser session and forwards the issuer's token, and the console verifies issuer and audience. It never accepts a ServiceAccount token. Two gates decide who enters: the client's requires at the issuer, and the gateway's groups posture for narrowing a route. See the Envoy Gateway recipe and oauth2-proxy.

A service with a console and an API uses two listeners, or one where every route names its proof. The API is reached by Service DNS and never by a public hostname. sluis serves both on one listener. The console takes the browser's session. The API takes an issuer token or a ServiceAccount token, and service_account matchers decide what that workload holds. The GitHub and Slack controllers are those callers.

When an API admits both anchors, the grant is keyed by the principal, so a consumer proven either way gets the same answer.

Admission is not authorization

The directory API is not served. A consumer of it would hold a grant along three axes: which directory, which groups, which questions. Outside the grant, answers look like an unserved domain, never like a refusal. See contracts.

What the rule rules out

The Go module offers two verifiers, Issuer and Cluster, both yielding one Verified with Groups []string. See the Go module and the TypeScript package.

The rule forbids:

  • A service verifying another cluster's key set directly.
  • A shared secret between services.
  • A roles claim beside groups.
  • A library that re-maps group names.
  • A console that accepts ServiceAccount tokens.
  • An API listener that gates on a browser session.

Decided in