sluisctlversion# sluisctl <version> (also --version, -version)
sluisctllogin--issuerhttps://access.example
sluisctlwhoami# who you are, and what it opens
sluisctlsetup# both of the next two
sluisctlkubeconfig# a context per granted cluster
sluisctlaws-config# a profile per granted cloud role
kubectl--contextstaginggetnodes# exec plugin: sluisctl kube-token
aws--profiledeployer@111122223333stsget-caller-identity# credential_process: sluisctl aws
sluisctltoken--audienceopenbao# one token for one audience, on stdout
sluisctlgithub-token--apppublisher--repositoryapp--permissioncontents=read
sluisctlexchange--audiencek8s:staging<subject-token
sluisctlbao--addresshttps://openbao.example:8200kvget-format=envsecret/app>.env
sluisctlbao--addresshttps://openbao.example:8200ssh-mode=ca-role=user[email protected]
sluisctlbao--addresshttps://openbao.example:8200write-field=signed_key\ssh/sign/userpublic_key=@key.pub>key-cert.pub
sluisctlpsql--addresshttps://openbao.example:8200-nsstaging---hdb.example-dorders
sluisctlpg--addresshttps://openbao.example:8200-nsstaging--pg_dumporders>orders.sql
sluisctlr2--service-urlhttps://r2-broker.example.com--credentials--bucketexample-bucket--prefixnix/
sluisctlbackupstatus# through the issuer you signed in at; --direct uses your AWS credentials
sluisctlrestorestart20261010T020000Z-3fa9c1--confirmexample--wait
sluisctlrender--installationinstallation.yaml--outrendered/# the service and policy documents
sluisctlpolicyrenderpolicy/-opolicy.yaml# the one policy document an installation reads
--audience, --issuer, --client; the client defaults to what login wrote. aws also takes --role
Issuer scheme
An issuer must be https://. http:// is accepted for localhost, 127.0.0.1 and ::1 only. The sign-in callback on the loopback port ignores a request that does not carry the sign-in's state
Issuer choice
--issuer, else $SLUISCTL_ISSUER, else the issuer: key of config.yaml (these three pin the issuer), else the one issuer with a live session. When none is pinned and more than one issuer has a live session, the command exits 2 and lists them, rather than using the most recent login. A session counts as live if its file parses, holds a refresh token and does not record an expiry in the past. With no live session the last login is used, so the error is "not signed in". login without --issuer uses the pin, else the last login
login
Authorization code with PKCE on a loopback port. The policy client is kind: public with a loopback redirect and sign_in_exchange: true, else later commands exit 4. Then it saves the issuer's /.well-known/sluis-configuration in backup: of config.yaml, when the issuer serves one; a failure does not fail the sign-in
version
sluisctl <version>, or sluisctl dev without release ldflags, with commit and date when stamped. --json prints {"version","commit","date"}. No network, config or HOME
token --show
Prints the claims of the token it was just issued, not the token: iss, sub, aud, groups, exp and iat as UTC times, then any other claim under claims. Decoded locally, not verified; never the compact token or its signature. YAML, or --format json
github-token
--app <id>, --repository <name> (repeatable; none needs an all-repositories grant), --permission <name>=<level> (repeatable; none asks for the grant), --json prints {"token","expires_at","repositories","permissions"}. See GitHub App tokens
credential ssh\|db\|client
Removed in v1.34.0; the error names the replacement
Legacy identifiers
Renamed in v1.76, with a compatibility period: the default OIDC client id is sluisctl; with no --client and no clientId: in config.yaml, login asks the issuer whether it knows sluisctl and, on invalid_client, signs in as accessctl once, with a deprecation notice on stderr (an explicit id never falls back, and the session remembers the client it ran as). The kubeconfig user is sluisctl:<cluster>, and sluisctl kubeconfig renames an existing accessctl:<cluster> user in place, repointing its contexts. The managed file is ~/.ssh/known_hosts.d/sluisctl, copied once from accessctl (the old file stays; marker .migrated-from-accessctl). Still read: env ACCESSCTL_* (SLUISCTL_* wins), config directory accessctl (copied once), AWS session-name fallback
Client migration
Register a sluisctl client (public, loopback redirect, same requires) beside accessctl in every estate's policy before workstations upgrade; retire accessctl one release later. A clientId: accessctl line that an older login wrote into config.yaml is an explicit value: delete it to move to sluisctl
credential_process JSON {"Version":1,"AccessKeyId","SecretAccessKey","Expiration"}, no SessionToken. --file reads a projected cloudflare/v1 document, signs in to nothing and refuses an expired one
Detail
Behavior
Flags
Both take --issuer and --client; CI uses the job identity. --lifetime is at most the preset's
--no-cache, SLUISCTL_CLOUDFLARE_NO_CACHE=1
Neither reads nor writes the credential cache and takes no cache lock, so a minted token is never written to disk and every call mints a new one. Use it where a program execs sluisctl and the token must not outlive it. The flag or a true value of the variable (1, true) turns it on. The sign-in session cache is not a credential cache and is unchanged. --file caches nothing either way
Exit 4
Not granted, or nonexistent
aws-config
Adds profile <preset>@r2 per granted R2 preset: credential_process = sluisctl cloudflare r2 <preset> --issuer <issuer>, endpoint_url, region = auto, request_checksum_calculation and response_checksum_validation = when_required, s3 = with addressing_style = path
whoami
Lists granted presets
render: an installation in, the two documents out¶
policy render builds the one policy document from layers, alone or as a directory read in name order. Without -o it writes to stdout. Name it in policy.file, or pass it as chart policy (without apiVersion) or Pulumi Policy.
Layer
Form
Policy file
version: 1
Access document
access: and overlay:, reshaped into tables
Fragment
apiVersion: sluis.truvity.github.io/policy/v2: tables and the sections exchange, apps, controllers
Merge
Rule
Tables
By key; a key declared twice is an error naming the file
Lists, name lists
Concatenate, union
Scalars
Declared twice is an error
The result passes every check the service runs at start. Exit codes as render.
Reads /sluis/<instance>/internal/oidc/recovery-password with your own AWS credentials, the SDK's default chain, and prints it decoded. --account conformance reads internal/oidc/conformance-password, which exists when conformance.enabled is set
recovery verifier
Reads a password from standard input and prints the account's SRP credential: the random salt, the verifier and the PBKDF2 rounds. Store it as the secret recovery.verifierSecret or conformance.verifierSecret names. For an installation that is not a Pulumi stack. Refuses a password that is weak or not printable ASCII
Detail
Behavior
--instance
Else $SLUISCTL_INSTANCE, else backup.instance in config.yaml
--region
Else the AWS configuration's, else backup.region in config.yaml
Output
Standard output must not be a terminal: pipe it or pass --show
<config> is the OS user configuration directory (~/.config/sluisctl on Linux). Files are 0600 in 0700 directories, written then renamed. A cache that is absent, expired or unwritable means mint afresh.
File
Holds
<config>/config.yaml
clientId and lastIssuer (the last sign-in, not a pin), written by login; issuer: pins the issuer and is only ever set by hand. A file written by an older release holds the last login under issuer:, which therefore counts as a pin until that line is removed
<config>/sessions/<issuer>-<hash>.json
One per issuer: refresh token, last access token and expiry. A stored issuer that disagrees with the lookup reads as not signed in. Rotated tokens are written back; a refused refresh reads as not signed in
<config>/sessions/<issuer>-<hash>.json.lock
A refresh holds it, so concurrent commands do not spend the refresh token twice
<config>/session.json
The single-issuer session of older releases, read for any issuer without its own file. Never deleted; a login writes that issuer's file, which wins
Cache
Path
Keyed by
Offered until
kube-token
<config>/kube/<hash>.json
the issuer, the client id and the audience, hashed together
a minute before the token's expiry, which is when client-go re-runs the plugin
aws
<config>/aws/<hash>.json
the audience and the role ARN, hashed
five minutes before the credential's expiry, when the AWS SDK would refresh its own copy
cloudflare
<config>/cloudflare/<hash>.json
the issuer, the client id, the preset and the lifetime asked for, hashed together
with a third of the credential's lifetime left (the lifetime is stored beside it)
r2
<config>/r2/<hash>.json
the issuer, the client id and the audience, hashed together
a minute before the token's expiry, the same margin kube-token uses
A lock beside each file makes many callers one exchange. login touches no cache; delete a cache directory to force a fresh exchange.
Through kubectl config. Writes the credential and context, not the cluster entry; the address and CA come from the platform's kubeconfig or aws eks update-kubeconfig
AWS config
Rewrites everything between # >>> sluisctl >>> and # <<< sluisctl <<< on each run. A profile is <role>@<account> with credential_process = sluisctl aws --audience aws:<account>:<role>; no secret
In a job granted id-token: write, kube-token, aws, token, github-token, bao, r2, pg and psql exchange the job's identity token (GitHub Actions). No login or session cache; per-credential caches still apply.
sluisctlclientsrotate<id>[--overlap24h]# a new secret; the old one stays valid for the overlap
sluisctlclientsshow<id># metadata only, never a secret
sluisctlclientspurge<id># delete the record of a client no longer generated
Detail
Behavior
Flags
--issuer, --client; --overlap is 0 to 168h, default 24h; 0 cuts the old secret at once
Caller
An operator with a token for the default client or console
Signed in (the issuer from --issuer, $SLUISCTL_ISSUER, issuer: or the one live session), backup run\|status\|list and restore preview\|status go through the issuer with the login's token. No AWS credentials. --direct uses the caller's own AWS credentials, the SDK's default chain, so a profile from aws-config works. So do --endpoint, --function, --profile, --region and --as breakglass, which imply --direct. Without a login every command is direct. See routes
Always direct
restore start and restore resume never go through the issuer, so a restore works when the issuer is broken or lost. They never read the issuer's document either: the target is --endpoint, else the environment, else the copy saved at login, and --confirm is held to --instance, $SLUISCTL_INSTANCE or the saved backup.instance, never to the document
Issuer refusals
contended is an outcome, not an error. 4 for a group the policy does not name (backup run needs backup.runGroups) or a role short of viewer or operator; 3 when not signed in; 5 when the issuer cannot be reached. --direct is the way round an issuer without a backup module
Endpoint
For everything but restore start and resume: --endpoint, else $SLUISCTL_BACKUP_ENDPOINT or $SLUISCTL_RESTORE_ENDPOINT, else the issuer's live document, else backup.endpoint or backup.restoreEndpoint in config.yaml, which login saves. A failed fetch says so on stderr and uses the saved copy. A Lambda function ARN is the only transport implemented; another scheme exits 2
Old names
--function is a deprecated alias of --endpoint. $SLUISCTL_BACKUP_FUNCTION, $SLUISCTL_RESTORE_FUNCTION and the function and restoreFunction keys still work, as an arn: endpoint or a bare function name
Saved
backup.instance and backup.region too. Addresses, no secret. --region overrides the region, which is otherwise the endpoint's
Class
--as admin (default) or breakglass: every direct call goes through the alias live-<class>
Backup
Module calls backup.run (direct: synchronous; through the issuer: started, then backup.status is read until the run ends, up to 16 minutes, or --no-wait prints started; --resume answers idle since format 2), backup.status, backup.list. Verifying a backup is sluis backup verify, with the backup document (backup)
--json
The same shape on both routes. Through the issuer, retention in backup status holds the counts in the text, not the ids of the backups removed
restore preview
Counts per module and section. Direct, a synchronous restore event with preview, names in --json; through the issuer, never names, and a refusal without its cause
restore start
Refuses unless --confirm equals the instance (--instance, $SLUISCTL_INSTANCE, backup.instance as login saved it, else the issuer's live document). Sends the event asynchronously with by set to the caller's sts:GetCallerIdentity ARN, then looks up the run and prints its id. --wait polls restore.status every 5 seconds until completed, or failed (exit 1)
backup run --resume
Only continues a paused run, and since format 2 none pauses: it answers idle and starts no backup. Kept for old scripts
restore resume
Does nothing when no restore is unfinished
IAM
Direct only: lambda:InvokeFunction on the function's live-admin or live-breakglass alias; a refusal exits 4