backup (default) or restore: which function the zip is. The restore function's role may write every module's table
backup.recipients
The age X25519 public keys (age1...) every backup is encrypted to. Required in the backup role, at least one. In the restore role they are the recipients a backup's sidecar must list, no more and no fewer. That catches a backup written before the recipients changed, not a forgery; see Provenance
backup.verify
Restore role: where the audit trail is and how to ask it. query (https, or http on loopback), tokenFile, and one of observer and anyObserver: true are required; profile defaults to activity
backup.identity
The restore role's secret holding the age identity (AGE-SECRET-KEY-1...): internal/backup/<identity>, default restore-identity, stored as {"value":"<base64>"}. Only the restore role reads it
backup.key, backup.region, keys.archive
Deprecated and ignored since format 2; accepted for one release with a warning
backup.target
The archive bucket: bucket, prefix, region, kmsKey, endpoint, pathStyle. Credentials are the role's. Object Lock retention must not exceed retention.maxAge
backup.target.checksum
The integrity header of every upload, as ports.blob.s3.checksum: sha256 (default, x-amz-checksum-sha256, precomputed), md5 (Content-MD5) or none. An Object Lock bucket refuses an upload without one
backup.retention
The keep newest backups (default 7) always stay. Another goes when older than maxAge (default 720h)
The backup: a PAX tar, gzip, age to the recipients, binary. The id is 20261013T020000Z-ab12cd: UTC start and a suffix
backup/<instance>/<id>.json
The sidecar, written last: format, instance, id, created, creator, layout, object (key, size, sha256), recipients, and per module stateRecords, secretRecords, blobs, blobBytes. No secret names, no values
Tar member, in order
Content
manifest.json
format, instance, id, layout, created, creator, per module state and secrets (records, sha256) and blobs (count, bytes), and files: the SHA-256 of every other member
<module>/state.jsonl
One State or Index record per line. Modules come in the order oidc, github, slack, cloudflare, google
<module>/secrets.jsonl
One secret document per line
<module>/blobs/<key>
One blob, its bytes. Sorted by name
Every member
Mode 0600, the backup's start as its time, no owner names
Blob key escaping
Per /-segment: a byte other than a letter, a digit, -, _ or . becomes %XX. So does a leading .. An empty segment is %
Verification
Object size and SHA-256 against the sidecar, then age, then every member against the manifest and the counts, before the first write
# Offline, without sluis:
age-d-ikey.txt20261013T020000Z-ab12cd.tar.gz.age|tarxz
Take --config <file> and --json. A run that finds the lease held, or the module under maintenance, says so and exits 0
sluis backup run
A backup under the lease lease.backup:run, in one invocation. --resume answers idle
sluis backup list
The backups in the archive, newest first, from their unverified sidecars
sluis backup status
The latest runs and the last prune
sluis backup prune [--dry-run]
Applies the retention rule: the sidecar first, then the object; the object of a run with no sidecar after 7 days. A refused delete (Object Lock) is counted, not retried
sluis backup verify <id> [--identity <file>]
Read-only. Checks the object's size and SHA-256 against its sidecar; with an age identity file, decrypts it and checks every member against the manifest. It does not check provenance: the output says so, and a restore does
The restore role of this zip; on Lambda it is backup.role: restore. Take --config <file> and --json. Only an administrator or the break-glass role may invoke it
sluis restore preview <id> [--allow-unverified]
Per module and section: records to create, overwrite, leave, skip. Names, never values. Writes nothing, takes no lease. With the flag it is logged, not recorded in the trail
Restores. A destination that holds different data is refused unless --overwrite. A backup the sealed audit trail does not vouch for is refused unless --allow-unverified
sluis restore start --resume
Continues a restore that paused
sluis restore status
The latest restores and the modules still under maintenance
Step of a restore
Detail
Lease
lease.restore:run: one restore at a time. The record is rec.backup.restore.<id> (running, paused, completed, failed)
Maintenance
The flag is set in every module's own table, then 15 seconds pass for the modules' cache
Verify
Download the backup and check it against the audit trail (Provenance) before the identity is read
Write
Read the identity, decrypt the archive, verify every member, write, read every record back. A pause starts the next invocation itself
Lift
Only after a clean read-back
Failure
The flag stays. Reasons: not-empty, verify, archive, integrity, key (the identity does not open the backup), maintenance, clear. Run again to retry
Refusal
A start that changes nothing is refused: backup (no such backup), key, unverified (the audit trail does not vouch for it), recipients (the sidecar lists other recipients than backup.recipients)
completed, paused, failed, contended or refused. A failed restore does not fail the invocation
{"kind":"restore","resume":true}
the function itself, admin, breakglass
Continues the unfinished restore; idle when none
{"kind":"restore","backup":"<id>","preview":true}
admin, breakglass; invoke synchronously
The preview report. allowUnverified applies
restore.status
console, admin, breakglass
latest, unfinished, lastCompleted, maintenance
restore.preview
console, admin, breakglass
The preview report as counts per module and section. Without record names; a refusal carries no cause. The backup is verified; there is no allowUnverified here
A restore asks the audit trail whether a backup is genuine, because the archive bucket cannot say.
Step
Rule
Record
roster.backup.completed carries object_key, object_size, object_sha256, sidecar_sha256 and recipients (the SHA-256 of each age1...). It is written after the sidecar
Lookup
The query service, backup.verify.query, searches backup.verify.profile for roster.backup.completed records whose target is the backup. It reads every page, and more than 200 records of the pinned observer refuse the backup
Match
A record exists, every record agrees, and its installation, object key, size, hashes and recipients are those of the fetched backup. A record without hashes, made by an earlier release, is no evidence: the backup is unattested
Sealed
The hour holding each record has a seal. Before that the restore is refused with not sealed yet: wait for the notary and run it again
Observer
Only records the writer stamped with backup.verify.observer count. Another observer's record is ignored and logged at error level. With anyObserver, any sender of audit records can vouch for a backup, and a start logs a warning
Recipients
With backup.recipients, the sidecar lists only those, verified or not. No flag overrides it. The recipients are public, so this is not a check on forgery
Order
All of this happens before the identity is read
Token
The query token's grants must let it search and get on backup.verify.profile. A redirect to another origin is refused
Sealed means
A seal object exists for the hour, as the query service reports. Not yet checked: the seal's signature (audit verify --root does), and that the seal covers the record's object
Rotation
After backup.recipients changes, an older backup is refused. Set the earlier list to restore it, or use --allow-unverified
Case
Outcome
backup.verify unset, query service down, no record, a mismatch, an unsealed hour
refused, reason unverified. Nothing is changed and nothing is recorded but a log line
A record that disagrees with the backup, or a replaced backup
refused, reason unverified, verificationmismatch. No flag overrides it: the trail says the archive was tampered with
--allow-unverified
For unattested, unsealed and unavailable only. It needs backup.recipients. roster.restore.unverified is kept (delivery block) before the restore begins, and .started and .completed carry unverified: true and verification
--allow-unaudited
With --allow-unverified, when roster.restore.unverified cannot be kept, or no audit installation is connected. Without it the restore is refused as unaudited. With it, an error log line names the backup, the reason, by and note
Who asked
The record names the workload (lambda:restore, cli:restore) and the caller's own by (at most 256 bytes, control characters removed), which nothing verifies. The invoker is in CloudTrail's Lambda data events where those are enabled
A restore that paused
Continues against the hashes it began with and does not ask the trail again. A backup replaced while it was paused is refused
The sluisctl login token. The issuer calls the module as console, over the module transport the installation already uses
backup run\|status\|list, restore preview\|status
Status, list and restore status: the viewer role. restore preview: the operator role. backup run: a group in backup.runGroups of the policy
Direct
The caller's own AWS credentials, as admin or breakglass
Every command
IAM on the function's live-<class> alias
Direct only
The same
restore start, restore resume
The same. They work when the issuer is broken or lost
Policy key
Meaning
backup.runGroups
Groups of the policy whose holders may start a backup through the issuer. Unset lets nobody. The operators group is not implied
Detail
Behavior
Served
The console's BackupService, to the access token of a live sluisctl session and to no other service. A token of a session that ended, of an agent session, of another client, or an ID token is refused
Maintenance
RunBackup is refused while the backup module is under maintenance
Start
RunBackup hands the run to the module as an asynchronous call (a Lambda event invocation, or a background call to a Service) and answers started with the issuer's clock. sluisctl reads GetBackupStatus until the run ends, or stops with --no-wait
Throttle
contended when a run is already going, or when the same person started one in the last 2 minutes (per issuer process; the lease is the guard across replicas)
Policy
backup.runGroups is read when the issuer starts, like the rest of the policy. Removing a group takes a restart
Restore
No call of the console or the issuer starts or resumes a restore
GET /.well-known/sluis-configuration on the issuer
Callers
A person signed in at sluisctl: the access token of a live, non-agent session at that client. Anyone else, and an issuer without console.backup, gets 404
issuer
The issuer's URL
instance
instance, else release, else sluis. restore start --confirm must equal it
backup.endpoint, restore.endpoint
A URI whose scheme names the transport. arn:aws:lambda:<region>:<account>:function:<name> is the only one sluisctl reaches. k8s:// and https:// are reserved, and refused as a usage error
region
The region of a Lambda endpoint
Not listed
A bare function name in console.backup.function, and console.backup.url: a laptop cannot invoke either
sluisctl
login saves the document in backup: of config.yaml. A direct command fetches it again, and falls back to the saved copy with a note on stderr
outcome: completed, contended or maintenance. A failed run fails the invocation
{"kind":"backup","resume":true}
a <function>-resume schedule left by an earlier release (an apply of v1.76.5 or later deletes it)
idle, touching nothing
backup.status
console, admin, breakglass
latest, lastCompleted, unfinished, retention
backup.list
console, admin, breakglass
backups, newest first; limit
backup.run
console, admin, breakglass
A run's result; resume answers idle. From console, by names the person: the issuer checked their group first, and the audit records name them. The issuer starts it as an asynchronous call and does not wait. From admin and breakglass, by is ignored and the actor is the workload lambda:<class>
A CronJob for sluis backup run (backup.schedule, concurrencyPolicy: Forbid) and one for sluis backup prune (backup.prune), as their own account (backup.serviceAccount)
backup.config
The sluis-backup/v1 document of this page, rendered as it stands. The chart refuses a missing backup.target, missing backup.recipients for the CronJob, backup.role: restore, a layout other than v5 and a missing ports.dynamodb.tables
restore.enabled
One Job, sluis restore start <backupId> --confirm <confirm> or --resume, as an account of its own. Never a CronJob. confirm must be backup.config.instance
Example
charts/sluis/examples/backup-v5.yaml. The chart has one Deployment, not one per module
config.secrets.layout: v5
The service on layout v5 needs config.ports.dynamodb.tables, and the chart then projects no Secret: its secrets are in SSM