Skip to content

How does sluis connect to everything around it?

Every point where something outside talks to sluis is numbered once here. The structural drawing is architecture. The how-to for each relying party is under connect. What ships, as charts, packages and binaries, is in artifacts.

Every arrow rests on one of two trust anchors, chosen by scope (trust). The cluster anchors a workload calling a service in the same cluster. The issuer anchors everything further away.

Identity in

Directories, GitHub Actions and cluster ServiceAccounts give sluis its proofs (①–⑤). The issuer asks the directory who an address is.

flowchart TB
  gws["Google Workspace<br/>tenants"]:::ext
  entra["Microsoft Entra<br/>(later)"]:::ext
  gh["GitHub Actions<br/>OIDC tokens"]:::ext
  sa["Kubernetes<br/>ServiceAccounts"]:::ext
  hub["the directory<br/>inside sluis"]:::hub
  iss["sluis issuer<br/>issuer, console"]:::token
  gws -- "① Admin SDK reads" --> hub
  entra -. "① Graph reads" .-> hub
  gws -- "② OIDC sign-in" --> iss
  gh -- "③ token exchange" --> iss
  sa -- "④ token exchange" --> iss
  iss -- "⑤ who is<br/>this address" --> hub
  classDef ext fill:#8A93A3,stroke:#5E6675,color:#fff
  classDef hub fill:#0E7C7B,stroke:#0A5958,color:#fff
  classDef token fill:#4A4FB5,stroke:#33378A,color:#fff

Access out

The issuer is the trusted issuer of every relying party (⑥–⑩, ⑮). A gateway turns a login into a bearer for the consoles.

flowchart TB
  iss["sluis issuer<br/>the issuer"]:::token
  eks["Kubernetes<br/>API servers"]:::ext
  aws["AWS accounts"]:::ext
  argo["ArgoCD, Kargo"]:::ext
  proxy["gateway OIDC"]:::ext
  consoles["Consoles"]:::ext
  reg["ECR, CodeArtifact,<br/>any AWS service"]:::ext
  iss -- "⑥ issuer + client id" --> eks
  iss -- "⑦ issuer + audience" --> aws
  iss -- "⑧ static clients" --> argo
  iss -- "⑨ login" --> proxy
  proxy -- "⑩ forwarded bearer" --> consoles
  aws -. "⑮ --profile" .-> reg
  classDef ext fill:#8A93A3,stroke:#5E6675,color:#fff
  classDef hub fill:#0E7C7B,stroke:#0A5958,color:#fff
  classDef token fill:#4A4FB5,stroke:#33378A,color:#fff

Clients and controllers

Libraries, sluisctl and the workflow action reach the issuer (⑪–⑬). The two controllers ask the console who holds each group and act in GitHub and Slack (⑭, ⑲).

flowchart TB
  lib["Go module, TS package<br/>inside applications"]:::token
  ctl["sluisctl<br/>laptops and jobs"]:::token
  act["exchange action<br/>in workflows"]:::token
  iss["the issuer"]:::token
  consoles["Consoles"]:::ext
  ghr["GitHub controller<br/>a loop in the GitHub module"]:::token
  slr["Slack controller<br/>a loop in the Slack module"]:::token
  ghteams["GitHub organisations"]:::ext
  slack["Slack workspaces"]:::ext
  lib -. "⑪ reads the bearer" .-> consoles
  ctl -- "⑫ code + PKCE,<br/>then exchange" --> iss
  act -- "⑬ exchange" --> iss
  iss -. "⑭ ⑲ who holds<br/>each group" .-> ghr
  iss -. "⑭ ⑲ who holds<br/>each group" .-> slr
  ghr -- "⑭ invites, teams,<br/>removals" --> ghteams
  slr -- "⑲ invites, removals,<br/>channels" --> slack
  classDef ext fill:#8A93A3,stroke:#5E6675,color:#fff
  classDef hub fill:#0E7C7B,stroke:#0A5958,color:#fff
  classDef token fill:#4A4FB5,stroke:#33378A,color:#fff

Case by case

Cases ⑥, ⑦ and ⑧ read aud as the client asking. A client may instead name a resource it wants a token for (RFC 8707), gated by that resource's own requires. A client the installation does not deploy may present a URL as its client_id, admitted only from an allow-listed origin. Both are off unless declared: see policy.

# Case Anchor What happens Guide
① A corporate directory to sluis the directory's own OAuth Admin consent or a service-account key with domain-wide delegation. sluis discovers the tenant and its domains and re-reads on a schedule. Corporate directory
② A person signs in the issuer; the corporate IdP is the proof Routed to the tenant by domain, MFA there, then the issuer asks the directory (⑤) and mints. Design, Google Workspace
③ A CI job the issuer; GitHub's token is the proof The job exchanges GitHub's identity token for a client's audience, gated by an owner allow-list and matchers on repository, ref and visibility. GitHub Actions
④ A workload the cluster that issued the token, by its published key set Exchange against that key set. sluis never calls TokenReview. Service to service
④b An AWS workload the AWS account that issued the token, by its published key set The IAM role asks STS for an outbound-federation token and exchanges it. One row per account, matchers on account, role and path. AWS workloads
⑤ The issuer asks the directory none A read of the snapshot that the Google module refreshes. A failure is an error, never an empty answer. Design
⑥ A Kubernetes cluster the issuer; the API server reads groups One OIDC provider and one public client per cluster, RBAC bound to internal group names as the policy spells them. Kubernetes cluster
⑦ An AWS account the issuer; the trust policy reads aud One IAM OIDC provider per account, one trust policy per role, AssumeRoleWithWebIdentity. AWS account
⑧ ArgoCD and Kargo the issuer Their own OIDC login reads groups through their own policy. ArgoCD, Kargo
⑨+⑩ A console behind a proxy the issuer Gateway-native OIDC on Envoy Gateway, or oauth2-proxy run by hand on any other gateway. Console app, business surface, oauth2-proxy
⑪ An application reads who is calling the listener's own anchor The Go module or TypeScript package verifies a bearer and reads groups. Go module, TypeScript package
⑫ A person's laptop the issuer sluisctl login once. kubeconfig, aws-config, token, cloudflare, bao, r2, pg/psql and backup use the cached sign-in. sluisctl
⑬ A workflow the issuer, by exchange (③) One shell-only step exchanges the job's token and writes a kubeconfig and an AWS profile. GitHub Actions
⑭ GitHub teams a GitHub App per organisation The controller invites, adds, promotes and removes. This is the sync model. GitHub organisation, GitHub Apps, infrastructure as code
⑮ Registries and other AWS services an AWS credential from ⑦ --profile <role>@<account>, then the service's own login, such as ECR's credential helper or aws codeartifact login. Registries and artifacts
⑯ Break-glass the cluster, as the floor; the recovery password where there is no cluster A person mints a ServiceAccount token, or reads the recovery password, and signs in with it when the directory is broken. Lost operator access
⑰ A secret manager that mints certificates the issuer; the JWT mount reads aud and groups Exchange for openbao, log in on the mount, make one sign call, revoke the manager's token. OpenBao
⑱ An R2 credential broker the issuer; the broker verifies a standard OIDC token sluisctl r2 exchanges for the broker's audience and runs the broker's CLI. The broker maps group to bucket, prefixes and permission. R2 storage
⑲ Slack channels a Slack App per workspace The controller creates or adopts channels, invites people and, in a strict private channel, removes them after the directory vouches. This is the sync model. Slack Connect channels are console records and never lose members. Slack workspace, Slack Connect channels, Slack Apps
⑳ A secret store receives recovery copies and projected App credentials the store's own access External Secrets copies the Secrets the console writes that nothing else can re-deliver (directory.push, githubApps.push, slackState.push). A catalogue App with push also projects its credential. Off unless written out, and what lands there is a real credential. Back up and restore, GitHub App projection, Slack App projection
㉑ A Cloudflare API token or R2 credential the issuer, by exchange sluisctl cloudflare token exchanges for audience=cloudflare:<preset>. The Cloudflare module mints, under the policy's cloudflare.grants. Cloudflare tokens
㉒ An MCP server the issuer; the server reads aud A resources row names the server. A stock server sits behind the resource-proxy sidecar, which serves RFC 9728 metadata. MCP, resource proxy

Claims model and sync model

Cases ⑥, ⑦, ⑧, ⑩, ⑮, ⑰ and ⑱ are the claims model. The decision rides in a token, because a cluster, a cloud account or a session cannot call a directory.

Cases ⑭ and ⑲ are the sync model. sluis writes the decision where it is enforced, because GitHub and Slack accept writes. Both models draw on the same directory and policy. The relying party's capabilities choose the model.

Two anchors

Only ⑯ stands on the cluster: recovery, the way in when the directory is broken, which depends on nothing else. Everything else stands on the issuer, including ④, because sluis proves a workload against the key set its own cluster publishes.

Decided in

  • ADR 0002: mission boundary: tokens and memberships
  • ADR 0008: credentials only where we govern membership
  • ADR 0030: workload identity
  • ADR 0071: one process per credential holder