The Pulumi library¶
Source: deploy/pulumi. Identifiers: Go packages. Decided in ADR 0037.
| Component | Type token | Creates |
|---|---|---|
NewStorage |
sluis:aws:Storage |
The blob bucket |
NewStates |
sluis:aws:States |
One DynamoDB table per module (ADR 0072) |
NewLambda |
sluis:aws:Lambda |
Function, role, HTTP API, signing key, schedules. The main path |
NewBackup |
sluis:aws:Backup |
The backup function, its role, aliases, schedules and alarms; optionally the archive bucket |
NewRestore |
sluis:aws:Restore |
The restore function: backup.role: restore, its role, two aliases, the resource policy and an alarm |
NewKubernetesIdentity |
sluis:aws:KubernetesIdentity |
The EKS Pod Identity role of the one pod |
Pass the provider with pulumi.Providers(p). RenderPorts and RenderPortsYAML render ports:.
store, _ := sluispulumi.NewStorage(ctx, "acme", &sluispulumi.StorageArgs{BucketName: "acme-prod-sluis"}, pulumi.Providers(awsProvider))
states, _ := sluispulumi.NewStates(ctx, "acme", &sluispulumi.StatesArgs{Modules: sluispulumi.ModuleSet{Instance: "acme"}}, pulumi.Providers(awsProvider))
ids, _ := sluispulumi.NewKubernetesIdentity(ctx, "acme", &sluispulumi.KubernetesIdentityArgs{
ClusterName: "acme", ClusterArn: clusterArn, AccountID: accountID,
Namespace: "sluis",
PermissionsBoundaryArn: boundaryArn,
ServiceAccount: "sluis",
Storage: store.Grant(),
State: states.Grant(),
}, pulumi.Providers(awsProvider))
Storage¶
StorageArgs |
Default | Meaning |
|---|---|---|
BucketName |
Required without Blobs |
Bucket name; global |
Blobs |
None | ExternalBlobs for an S3-compatible endpoint (R2). One of BucketName and Blobs; with Blobs no bucket is created and Versioning, ProtectedPrefixes, Tags are refused |
Versioning |
Off | S3 versioning |
ProtectedPrefixes |
None | Prefix and AllowedPrincipalArns: a bucket-policy Deny of writes under the prefix to every other principal (truststore) |
Tags |
None | On the bucket |
| Detail | Behavior |
|---|---|
| Outputs | BucketName, BucketArn (empty with Blobs), Grant() |
| Bucket | Protected, AES256, public access blocked, non-TLS denied |
| Old sealer | Run pulumi state unprotect <urn> on <name>-sealer-key and <name>-sealer-alias before the first apply |
ExternalBlobs |
Meaning |
|---|---|
Bucket, Endpoint (https), Region (default auto), PathStyle, Prefix |
The store |
CredentialsRef |
Address internal/<module>/<name>, for example internal/google/blobs-r2, or external/cloudflare/<preset>. It names the secret, never holds it. Set this or CredentialsPreset, not both |
CredentialsRef: external/cloudflare/<preset> |
The credential the Cloudflare module rotates for an R2 preset of Installation.Cloudflare.Presets: the preset must be declared, have an endpoint, keep store: external and be at the blobs' Endpoint. Every function that has the blobs reads that one cloudflare/v1 document (SluisBlobCredentials); none holds the minter. The Cloudflare function mints for its own blobs, so it starts before the first rotation. This is the form for an installation with module functions. The adapter reads the document again with a third of its time left and once after a 403, and keeps nothing on disk. Choose lifetime longer than rotation plus the schedule's tick and five minutes (for example lifetime: 1h, rotation: 20m, a tick of 15m). Rotate the preset once (the first tick, or Rotate on the console's Cloudflare page) before the other functions switch to the address |
CredentialsPreset |
A key of Installation.Cloudflare.Presets with an endpoint (R2): sluis mints the store's credentials in process and the document gets ports.blob.s3.credentials.preset. No parameter and no SluisBlobCredentials grant. It works where the one function holds the minter (presets rendered into it, as on Kubernetes or without module functions); with module functions the presets are the Cloudflare module's, no other function can mint, and the library refuses it and names the CredentialsRef to use. Exclusive with CredentialsRef |
With Blobs |
Behavior |
|---|---|
| Document | Writes ports.blob (adapter: s3). A document naming ports.blob, adapters.blobs or endpoint is refused |
| IAM | With CredentialsRef: ssm:GetParameter on /sluis/<instance>/<CredentialsRef>, decrypted through SSM with ParameterKeyArn. With external/cloudflare/<preset>: the same grant on /sluis/<instance>/external/cloudflare/<preset>, nothing else under external/cloudflare/ and no minter parameter. With CredentialsPreset: none |
| Seed | With CredentialsRef, the s3-credentials/v1 parameter, out of band (blobs on R2) |
State¶
One DynamoDB table per module (ADR 0072).
StatesArgs |
Default | Meaning |
|---|---|---|
Modules |
Required | ModuleSet: Instance; Tables overrides a name (default sluis-<instance>-<module>) |
Only |
Every module | The modules that get a table |
KeyArn |
None | Customer-managed key for every table; otherwise the AWS-owned key |
Tags |
None | On every table |
| Detail | Behavior |
|---|---|
| Outputs | Tables, TableArns (by module), Grant() |
| Table | Protected, deletion protection, on-demand, point-in-time recovery, TTL on expires; only pk and sk declared; no secondary index |
| Grant | A role gets the tables of the modules it hosts |
| Ports | PortsArgs.Tables renders ports.dynamodb.tables; the document needs secrets.layout: v5 beside it |
Kubernetes identity¶
One role for the one pod (switch the serving pod role).
KubernetesIdentityArgs |
Default | Meaning |
|---|---|---|
ClusterName |
Required | EKS cluster |
ClusterArn, AccountID |
Required | Pin the trust policy to the cluster |
Namespace, ServiceAccount |
Required | The one ServiceAccount; it takes one association |
Region |
Provider's | On each association |
PermissionsBoundaryArn |
None | Boundary of the role |
RoleNamePrefix |
Component name | Role and managed policy are <prefix>-sluis |
Description |
Generated | Policy description; a change replaces the policy |
Instance, Region, ParameterKeyArn |
None | With Instance, the role has the module paths of the layout (internal/<module>/*, external/<module>/*) and internal/config/* (read) under /sluis/<instance>/; Region becomes required |
Storage |
Required | Storage.Grant() |
State |
None | States.Grant(); nil for the legacy store |
SigningKeyArns |
None | SigningKeyArn, SigningKeyRS256Arn: allows kms:Sign, kms:GetPublicKey |
WrappedSigningKeyArn |
None | Symmetric key for kms-wrapped (signing on AWS). List this role in WrappedSigning.AdditionalSigningRoleArns |
| Outputs | n/a | RoleArn, RoleName |
The role trusts pods.eks.amazonaws.com for this cluster and ServiceAccount.
| Sid | Actions | Resource |
|---|---|---|
SluisBlobs |
s3:GetObject, PutObject, DeleteObject |
Bucket objects |
SluisBlobList |
s3:ListBucket |
The bucket; without it a missing key reads 403, not 404 |
SluisSigning |
kms:Sign, kms:GetPublicKey |
Signing keys |
SluisWrappedSigning |
kms:GenerateDataKeyPairWithoutPlaintext, kms:Decrypt |
Symmetric key; only with kms:EncryptionContext:purpose = sluis-signing and no context keys besides purpose, alg, kid |
SluisState |
dynamodb:GetItem, PutItem, UpdateItem, DeleteItem, Query, Scan, DescribeTable |
The module tables |
SluisStateKey |
kms:Encrypt, Decrypt, GenerateDataKey, DescribeKey |
The table key, only through DynamoDB (kms:ViaService) |
Backup¶
BackupArgs (BackupCommon) |
Default | Meaning |
|---|---|---|
| Function | n/a | The release zip as the backup function on layout v5 (ADR 0072); the configuration layer holds one sluis-backup/v1 document |
Region, AccountID, Instance |
Required | Name the resources; the archive path is backup/<instance>/ |
Package, PackageSHA256, PackageVersion, Artifacts, Release |
As LambdaArgs |
The release zip, byte for byte |
Storage, BlobBucketName, BlobPrefix |
Required | The blob store: ports.blob. BlobBucketName is not needed with Storage.External |
State, TableNames |
Required | States.Grant() with all six tables; ports.dynamodb.tables |
ParameterKeyArn (required), MinterRefs |
ParameterKeyArn: none, and the plan is refused without it |
secrets.kmsKeyId, and the key of the restore identity: a SecureString left to SSM would be encrypted with the AWS-managed key. MinterRefs: the Cloudflare minter parameters the backup role reads, read only |
ArchiveKeyAlias |
None | Deprecated. Backups are age-encrypted (format v2) and no role holds a KMS grant for the archive. While set it is held to an alias/<name>, rendered as backup.key for a binary of format v1, and ArchiveKeyArn names the key behind it |
ExtraRecipients |
None | BackupArgs only. Further age recipients (age1...) every backup is encrypted to, after the installation's generated one: an offline break-glass key is recommended. Each is parsed as an age X25519 recipient; an invalid one is refused, a repeat is dropped |
Archive |
Required | Bucket, Prefix, Region, AccountID, Versioned (true), SSEKeyArn, RestoreRoleArn, Create |
Archive.Create |
nil | ArchiveBucketArgs: Mode (required: GOVERNANCE, or COMPLIANCE with AcknowledgeCompliance; empty was GOVERNANCE and is refused, because COMPLIANCE cannot be undone and a default must not move the lock of a bucket that exists), RetentionDays (Retention.MaxAge in days), Tags |
Archive.BreakglassRoleArns |
Required with Archive.Create |
Role ARNs (a * only in the hash of an SSO role's name) that alone may bypass the lock. The bucket's policy gets SluisArchiveLockBreakglassOnly: a Deny of s3:BypassGovernanceRetention, s3:DeleteObjectVersion and s3:PutBucketObjectLockConfiguration to every other principal (ArchiveBucketGuardStatement). Given for an existing bucket, the same statement is added to the ArchiveBucketPolicy output for the bucket's owner to apply. Include the role that applies the stack |
Retention |
7; 720h | Keep, MaxAge: backup.retention. An Object Lock longer than MaxAge is refused |
AuditQueueURL |
None | adapters.audit sqs; without it the log adapter |
FunctionName, Function, LogRetentionDays |
sluis-<instance>-backup; 512 MB, 900 s; 30 |
Function.TimeoutSeconds is 60 to 900 |
Schedule |
cron(0 2 * * ? *); false |
Daily, Paused. ResumeRate is deprecated and ignored: the <function>-resume schedule is gone (a run of format 2 does not pause), and an apply deletes it |
AlarmActionArns, PermissionsBoundaryArn, Tags |
None | Notified by the alarms; boundary of the roles |
Telemetry, AdditionalWebIdentityAudiences, CodeSigningConfigArn |
None | As LambdaArgs: the telemetry layer and its extra audiences (only with Telemetry), and the code signing configuration |
| Output | Meaning |
|---|---|
FunctionArn, FunctionName, RoleArn, RoleName, ConfigLayerArn |
Function, role and layer |
LiveAliasArn, AliasArns |
live, which the schedules invoke, and live-console, live-admin, live-breakglass; the alias is the caller class of a module call |
SchedulerRoleArn, ScheduleNames, ScheduleDLQArn |
<function>-daily sends {"kind":"backup"}; undelivered events go to the queue |
ArchiveBucketName, ArchiveBucketArn |
The archive |
ArchiveKeyArn |
Deprecated: the key behind ArchiveKeyAlias, empty when it is not set |
RestoreRecipient |
The age1... recipient of the installation's generated restore keypair. Public |
Recipients |
Every recipient of backup.recipients: RestoreRecipient, then ExtraRecipients |
RestoreIdentityParameter |
/sluis/<instance>/internal/backup/restore-identity, the SecureString parameter that holds the age identity |
ArchiveBucketPolicy |
The policy a bucket owner in another account applies, from ArchiveBucketPolicyStatements |
AlarmNames |
The three alarms below |
| Alarm | Fires when |
|---|---|
<function>-no-backup-36h |
The log line a backup was written is absent for 36 hourly periods; missing data counts |
<function>-failed-run |
The function's Errors is at least 1 in five minutes |
<function>-schedule-dlq |
The dead-letter queue holds a message |
| Backup role grant | Actions | Resource |
|---|---|---|
SluisTableBackup |
GetItem, PutItem, UpdateItem, DeleteItem, Query, Scan, DescribeTable |
The backup table |
SluisCrossTable, …Parameters, …Blobs, …BlobList (one statement per kind, the five modules in its resource list) |
Read: GetItem, Query, Scan; ssm:GetParameter; s3:GetObject and the prefix's ListBucket |
The other five modules' tables, internal/<module>/*, external/<module>/*, <module>/ |
SluisCrossParametersList |
ssm:GetParametersByPath |
internal/* and external/* of the instance, never the namespace roots: the export lists each module's parameters before it reads them by name. A listing decrypts nothing |
SluisMaintenanceDeny |
Deny PutItem, UpdateItem, DeleteItem, BatchWriteItem, TransactWriteItems |
Every table, when the first key is maintenance. The role reads the flag and never writes it |
SluisArchiveObjects |
s3:GetObject, PutObject, DeleteObject, and GetObjectVersion on a versioned bucket |
<bucket>/<prefix>backup/<instance>/* |
SluisArchiveList |
s3:ListBucket, and ListBucketVersions on a versioned bucket |
The bucket, s3:prefix <prefix>backup/<instance>/* |
SluisArchiveSSEKey |
kms:Decrypt, GenerateDataKey |
SSEKeyArn, through S3 only |
SluisDenyRestoreIdentity |
Deny ssm:* |
The restore identity parameter. The role hosts the backup module, whose namespace the identity is in |
SluisDenyRestoreIdentityKey |
Deny kms:Decrypt |
Any key, when the encryption context PARAMETER_ARN is the identity parameter: closes GetParametersByPath, which is authorised on the path |
Keys¶
| Detail | Behavior |
|---|---|
| Format | v2 encrypts each backup with age to backup.recipients. The backup role holds no key that opens one |
| Keypair | NewBackup makes a 32-byte random.RandomBytes seed (secret in state, never rotated by an apply) and derives the X25519 identity and recipient from it |
| Recipients | The generated recipient, then ExtraRecipients, in the backup document. Keep an offline ExtraRecipients identity: it opens backups when the installation is gone |
| Identity | AGE-SECRET-KEY-1... in the byte codec ({"value":"<base64>"}), a SecureString at /sluis/<instance>/internal/backup/restore-identity under ParameterKeyArn. The restore document names it with backup.identity: restore-identity |
| Readers | The restore role, and the deployer: SluisKeyAdminReads lets KeyPolicyOptions.Admins decrypt it, as the provider does after create and on every refresh. The backup role is denied the parameter and its decryption (SluisDenyRestoreIdentity, SluisDenyRestoreIdentityKey); SluisKeyPolicyStatements adds SluisKeyDenyRestoreIdentity |
| Adoption | The parameter has overwrite: true: an apply whose earlier create succeeded in AWS adopts it, with the same value from the seed in state |
| Break-glass | age-keygen -o breakglass.txt offline; age-keygen -y breakglass.txt prints the recipient for ExtraRecipients (guide) |
| Rotation | pulumi up --replace on <name>-restore-identity-seed leaves older backups readable by an ExtraRecipients identity only |
| Deprecated | KeyPolicyOptions and KeyPolicyArgs BackupArchive and RestoreArchive, and ArchiveGrant.KeyArn, are accepted and add nothing |
| Helper | Renders |
|---|---|
BackupRoleStatements(env, ArchiveGrant) |
The role above, for an estate that builds its own |
ArchiveBucketPolicyStatements(grant, backupRoleArn, restoreRoleArn) |
The bucket policy for a bucket in another account; its owner also names the roles in the bucket key's policy |
Restore¶
RestoreArgs |
Default | Meaning |
|---|---|---|
BackupCommon |
As BackupArgs |
The same installation and archive. NewBackup of the same instance must be in the stack: it creates the restore identity this function reads. Archive.Create is refused: NewBackup makes the bucket. FunctionName defaults to sluis-<instance>-restore |
RestoreInvokers |
Required | AdminRoleArns and BreakglassRoleArns, at least one role each. A wildcard, an account root, a user, or a role in both lists is refused |
Function.TimeoutSeconds |
900 | At least 180, twice RestoreMargin (90 s): the slice a restore keeps back before it pauses and continues itself |
Recipients |
None | backup.recipients: pass Backup.Recipients of the same instance. A restore compares each backup's sidecar with them, and an unverified restore (allowUnverified) is refused without them |
Verify |
None | backup.verify (provenance): Query (https, or http on loopback), TokenFile (a file in the function's environment holding the query service's bearer token; the library provisions none), Profile, and one of Observer (the id the audit writer stamps on the backup function's records) and AnyObserver. Held to the restore's own rules at plan. Unset, every restore needs allowUnverified |
| Detail | Behavior |
|---|---|
| Function | The release zip with a document of backup.role: restore. No reserved concurrency, because lease.restore:run allows one restore at a time. No schedule |
| Aliases | live-admin and live-breakglass only. Both are asynchronous without retry |
| Resource policy | One lambda:InvokeFunction permission per invoker, on the alias of its class: administrators on live-admin, break-glass roles on live-breakglass. No unqualified permission, no service principal. Identity policies of other principals must not grant the invoke |
| Alarm | <function>-invoked: Invocations at least 1 in a minute |
| Policy size | The role's inline policy is near IAM's 10,240 characters; a long instance name can exceed it. The tests hold it on realistic ARNs |
| Outputs | FunctionArn, FunctionName, RoleArn, RoleName, ConfigLayerArn, AliasArns, IdentityParameter, ArchiveKeyArn (deprecated), AlarmNames |
| Restore role grant | Actions | Resource |
|---|---|---|
SluisTableBackup |
Table read and write | The backup table: rec.backup.restore.*, lease.restore:* and the maintenance flag |
SluisCrossTable, …Parameters, …Blobs, …BlobList (one statement per kind, the five modules in its resource list) |
Table read and write; ssm:GetParameter, PutParameter, DeleteParameter; s3:GetObject, PutObject, DeleteObject |
The other five modules' tables, internal/<module>/*, external/<module>/*, <module>/ prefixes, and the key through SSM |
SluisCrossCloudflareMinter |
ssm:GetParameter |
The MinterRefs parameters; the backup role only, never the issuer (it asks the cloudflare function to mint) or a module role |
SluisArchiveObjects, SluisArchiveList |
s3:GetObject, GetObjectVersion; ListBucket, ListBucketVersions |
<bucket>/<prefix>backup/<instance>/*. No put, no delete |
SluisRestoreIdentity, SluisRestoreIdentityKey |
ssm:GetParameter; kms:Decrypt through SSM |
The restore identity parameter internal/backup/restore-identity and its encryption context. The only function role that reads it; the deployer reads it back through SluisKeyAdminReads |
SluisRestoreSelf |
lambda:InvokeFunction |
Its own two aliases: a paused restore continues through the ARN it was invoked through |
| Maintenance flag | n/a | No SluisMaintenanceDeny: this role alone writes the flag in every table. RestoreRoleStatements(env, grant, selfArns) renders it |
Ports configuration¶
RenderPorts never renders create, writes only the blob without Tables and refuses any Adapter but dynamodb.
y, _ := sluispulumi.RenderPortsYAML(sluispulumi.PortsArgs{BucketName: "acme-prod-sluis", Region: "eu-west-1",
Tables: map[sluispulumi.Module]string{sluispulumi.ModuleOIDC: "sluis-acme-oidc", sluispulumi.ModuleGoogle: "sluis-acme-google"}})
ports:
adapter: dynamodb
blob:
adapter: s3
s3: {bucket: acme-prod-sluis, region: eu-west-1}
dynamodb:
region: eu-west-1
tables: {google: sluis-acme-google, oidc: sluis-acme-oidc}
Lambda¶
l, _ := sluispulumi.NewLambda(ctx, "access", &sluispulumi.LambdaArgs{
Region: "eu-central-1", AccountID: accountID,
Instance: "acme", // the SSM root /sluis/acme
Package: "dist/sluis-issuer_1.77.1_linux_arm64.zip", // or an https URL
PackageSHA256: "<the release's digest, pinned here>",
Installation: installation, // *sluisconfig.Installation: the library renders both documents from it
Storage: store.Grant(),
State: states.Grant(),
Audit: &sluispulumi.AuditArgs{
WriterPackage: "dist/audit-writer-lambda_<version>_linux_arm64.zip", WriterPackageSHA256: "<its digest>",
CatalogueDir: "dist/sluis-audit-catalogue", // the release's sluis-audit-catalogue bundle, unpacked
},
Schedule: sluispulumi.ScheduleArgs{GitHubOrgs: []string{"acme"}, SlackWorkspaces: []string{"T0ACME"}},
}, pulumi.Providers(awsProvider))
| Rule | Behavior |
|---|---|
| Package | Deployed byte for byte from a path or https URL. PackageSHA256 is required and checked; pin it in source. Library minor or newer (1.63) |
| Configuration | Immutable layer <prefix>-config at /opt/sluis, rendered from Installation, loader-checked (layer). A change publishes a new version |
| Endpoints | Refused unless AllowEndpoints |
Inputs (LambdaArgs)¶
| Field | Default | Meaning |
|---|---|---|
Region, AccountID |
Required | Name the SSM parameters and function in the role policy |
Instance |
Required | Lower-case letters, digits, dashes; not private or export. SSM root /sluis/<instance> |
Package, PackageSHA256, PackageVersion |
Required, required, from the file name | Zip, pinned digest, release when the name does not say |
Installation |
Required without Config |
The installation document. Rendered like sluisctl render; the library fills shape lambda, Instance, Region, AccountID and the function name when absent and refuses disagreement |
Config, Policy, PolicyPath |
Deprecated | Service document; policy or a layer directory (one of the last two). Removed after one minor; NewLambda warns |
AllowEndpoints |
false | LocalStack tests only; an R2 preset's endpoint needs no flag |
Storage, State |
Required | The grants; State is States.Grant() with the table of every module a function hosts |
TableNames |
sluis-<instance>-<module> |
Table name per module in ports.dynamodb.tables; the same map as StatesArgs.Modules.Tables |
Artifacts, Release |
Unset | Artifacts bucket |
Audit |
Install | Audit. Needs Installation |
AuditQueueArn |
None | Deprecated; exclusive with Audit. Required with Config |
ParameterKeyArn |
Required, unless Keys.Secrets names the key by alias |
Customer key for SecureStrings, written as secrets.kmsKeyId in every function's document (a module document written by the estate in Modules[<role>].Document must name the same key, compared as text (the alias for Keys.Secrets, the ARN for ParameterKeyArn), or it is refused); another is refused. The role may use it through SSM only. With neither, the library refuses to build: the state secret, the recovery password and every parameter the services write (GitHub and Slack apps, Google keys, the sign-in client) would be encrypted with the AWS-managed key alias/aws/ssm, which every role with ssm:GetParameter on them can decrypt |
KeyAdmins |
Required while the library creates a KMS key (Keys nil, and WrappedSigning without KeyArn); refused with Keys |
Deprecated with the keys it is for. Role ARNs (a * only in the hash of an SSO role's name) that alone hold the control plane of the library-created keys: key policy, grants, aliases, disabling and deletion. The keys carry a policy in which the root enables IAM for the key's use and upkeep and SluisKeyControlPlaneAdminsOnly denies the control plane to every other principal. Include the role that applies the stack |
CodeSigningConfigArn |
None | A Lambda code signing configuration (artifact.NewCodeSigningConfig) every function the library makes is put under. The release zips are not AWS Signer-signed: under an enforcing configuration the estate signs them first, or sets the configuration to warn |
Keys |
nil | Keys the estate supplies. Exclusive with the four deprecated key inputs |
SigningKeyAlias |
None, required unless Keys or WrappedSigning is set |
Deprecated: ES384 key alias, such as alias/sluis-<instance>-sign |
SigningKeyRS256Alias, DisableSigningKeyRS256 |
None, required unless Keys, WrappedSigning or DisableSigningKeyRS256 is set; false |
Deprecated: RSA key alias, such as alias/sluis-<instance>-sign-rs256; created unless disabled |
WrappedSigning |
nil | Deprecated: KeyArn (unset creates one), KeyAlias (none; required without KeyArn, such as alias/sluis-<instance>-wrapped). The asymmetric keys are not declared; signingKey.kms beside it is refused |
VerifyOnly |
None | []VerifyOnlyKeyArgs of an earlier signer: PEM (one public key or certificate: ECDSA on P-384, or RSA of 2048 bits or more), KeyID (default RFC 7638 thumbprint), Alg, Until (required). Written to /opt/sluis/verify-keys/<index>.pem and signingKey.verifyOnly. Private keys, EC keys other than P-384, RSA keys under 2048 bits, duplicates, a mismatched Alg and zero Until are refused |
Recovery |
Enabled | RecoveryArgs.Enabled (*bool) writes recovery.enabled; the password parameter exists either way (recovery) |
FunctionNamePrefix |
sluis |
Names <prefix>-scheduler, <prefix>-config and the function default |
FunctionName |
sluis-<instance>-issuer |
Function, role, policy and log group. aws.functions overrides it. A name kept from sluis or sluis-http replaces nothing (see the CHANGELOG upgrade note) |
Function |
512 MB; 300 s; unreserved | FunctionArgs: MemoryMB, TimeoutSeconds, ReservedConcurrency (*int, at least 1) |
Modules |
None | ModuleArgs by cloudflare, github, google, outline or slack: FunctionName (default sluis-<instance>-<module>), Package, PackageSHA256, MemoryMB, TimeoutSeconds (default Function's), ReservedConcurrency, Document. Every module function exists with or without an entry, except outline, which exists only with an outline document (the installation's controllers.outline). |
BackupFunctionName, RestoreFunctionName |
sluis-<instance>-backup, -restore |
The names the issuer's role and the key policy use for the backup and restore functions |
TransitionRoles |
None | Role and OldName: an old IAM role name kept as a further principal of the renamed role in the key policy and the audit queue's policy. Remove it once nothing runs under the old name |
Function.ReservedConcurrency |
nil | A ceiling on concurrent environments. It costs nothing and is not provisioned concurrency. It bounds a cold-start herd. A cap set by hand is removed by the next apply unless set here |
LogRetentionDays |
30 | Function log group |
AccessLogs |
nil (off) | RetentionDays (7). Declares log group /aws/apigateway/<FunctionName> and $default access logs: requestTime, requestId, httpMethod, path, status, responseLatency, integrationLatency. The query string, headers, address and identity are never logged. The applying principal needs logs:CreateLogDelivery, logs:PutResourcePolicy and related actions |
PermissionsBoundaryArn |
None | Boundary of the role and the scheduler's |
API.DomainName, .CertificateArn, .TruststorePEM, .TruststoreBucketName |
Deprecated | All four together keep the mutual-TLS domain with a warning. Move to the edge module (cutover) |
API.KeepDefaultEndpoint |
false | Keeps the execute-api endpoint for the acceptance suite; turn it off after, it bypasses the client certificate |
Schedule.GitHubOrgs, .SlackWorkspaces, .OutlineWorkspaces |
None | Targets, one schedule each. OutlineWorkspaces needs the outline function |
Schedule.OutlineRate |
rate(2 minutes) |
The Outline workspaces' schedule |
Schedule.Rate |
rate(5 minutes) |
EventBridge Scheduler expression |
Schedule.Paused, DirectoryRefresh.Paused |
false | Declares the schedules DISABLED, keeping roles and grants. Paused with Disabled is refused |
DirectoryRefresh.Rate, .Disabled |
rate(15 minutes), false |
Invokes {"kind":"refresh"} |
IssuerClients.Rate, .Disabled, .Paused |
rate(15 minutes), false, false |
With module functions: invokes the issuer with {"kind":"clients"}, the pass that settles the clients' generated secrets. Paused with Disabled is refused |
CloudflareRotation.Interval (or .Rate), .Disabled, .Paused |
15 minutes (rate(15 minutes)), false, false |
The tick, in whole minutes; Interval and Rate are exclusive. A credential rotates up to one tick after its rotation age, so every preset must satisfy rotation + tick + 5m < lifetime (checked for Interval and rate(...), at load). Invokes {"kind":"cloudflare"}. Exists when Installation.Cloudflare declares any preset, store: none included: the pass rotates the stored presets and deletes every preset's expired tokens. A store: none preset gets no margin check and no cloudflare.tick. Declaring presets also grants SSM read on internal/cloudflare/*, read and write on internal/cloudflare-minted/*, write on external/cloudflare/* (Cloudflare tokens) |
Check |
on (nil) | A post-deploy aws.lambda.Invocation of the live alias with {"kind":"check"}, after the function, its role policy and the parameters the stack writes exist. A declared secret that is missing, empty or malformed fails pulumi up with check: N of M declared secrets are missing or invalid, naming each address. Runs again when the function version or the declared names change. false leaves it out, for a CI step |
WebIdentityAudience |
Any | Restricts the audience of the role's web identity token |
AdditionalWebIdentityAudiences |
None | Extra exact audiences after it. Any code under the role can mint them. Empty, duplicate, or with empty WebIdentityAudience: refused |
Telemetry.LayerArn, .Env |
nil | The otlp-lambda layer; Env accepts OTEL_*, OPENTELEMETRY_*, the layer's SLUIS_* settings (the deprecated ACCESS_ROSTER_* prefix too), AWS_LAMBDA_EXEC_WRAPPER and nothing else. OTEL_SERVICE_NAME defaults to the function name. The layer's token comes from an issuer: one other than the function it observes keeps telemetry flowing while that function is saturated. The function drops what the layer refuses |
Tags |
None | On everything that takes tags |
Module roles¶
| Detail | Behavior |
|---|---|
| Role grants | ModuleRoleStatements of the hosted modules (oidc, github, slack, google, outline with its document, and cloudflare with presets): their tables, internal/<module>, external/<module>, <module>/ blob prefixes; SluisMaintenanceDeny on their tables; the oidc host is denied the recovery and conformance passwords (SluisDenyPasswords, SluisDenyPasswordsKey) |
| Parameters written | internal/oidc/state-secret, recovery-password and recovery-verifier; with conformance.enabled, also conformance-password and conformance-verifier. Each is the secret store's document ({"value":"<base64>"}). A verifier is derived from its password under a random salt kept with it (ADR 0076) |
| Service document | secrets.layout: v5 and ports.dynamodb.tables. A blob CredentialsRef is internal/<module>/<name>; internal/google/<name> is read by every process |
| Blobs from a preset | No role other than backup and the Cloudflare function reads the minter. A store whose credentials a preset rotates is read from external/cloudflare/<preset> (CredentialsRef) |
| Needs | State.Tables (States.Grant()) for the hosted modules |
| Removed | Layout, Compat, MigrationRoleArn, NewState and the legacy table: upgrade to v1.77.1 |
Outputs¶
| Output | Meaning |
|---|---|
SigningKeyArn, SigningKeyID, SigningKeyAlias |
ES384 signing key |
SigningKeyRS256Arn, SigningKeyRS256ID, SigningKeyRS256Alias |
RS256 key; empty when disabled |
WrappedSigningKeyArn, WrappedSigningKeyAlias |
Symmetric key of WrappedSigning |
CodeSha256Matches |
Lambda's code hash equals the verified zip |
FunctionArn, FunctionName, RoleArn, RoleName |
Function and role |
AccessLogGroupName |
Empty without AccessLogs |
APIID, APIStageName, APIURL |
HTTP API; APIURL answers only with KeepDefaultEndpoint. FrontDoor() returns the first two and the component name |
DomainTarget, DomainHostedZoneID, TruststoreBucketName, TruststoreURI |
Empty unless the deprecated API.DomainName is set |
SchedulerRoleArn, ScheduleNames |
Scheduler role and schedules |
LiveAliasArn, LiveVersion |
Alias live and its version |
ConfigLayerArn |
Configuration layer version |
DeclaredParameters |
SSM names the documents declare, below the root, sorted; set whether or not Check is on |
StateSecretParameter, RecoveryPasswordParameter |
SSM parameters of the OAuth-state secret and the recovery password, internal/oidc/... |
RecoveryVerifierParameter, ConformancePasswordParameter |
The recovery account's SRP credential, and the conformance password with conformance.enabled (empty otherwise) |
Audit |
*auditpulumi.Audit the library installed; nil with Use, Enabled: false or AuditQueueArn |
AuditQueueURL, AuditQueueArn |
The publish queue; empty when audit is off |
Artifacts bucket¶
Artifacts uploads the verified zip as is to a versioned bucket; function and layer use that object version.
| Input | Default | Meaning |
|---|---|---|
Artifacts.Bucket |
Unset (direct upload) | Must be versioned, else the apply fails |
Artifacts.Prefix |
sluis/ |
Keys are <prefix><version>/<sha256>-<file name> |
Release.ResolveChecksums |
false | Reads an empty PackageSHA256 from <BaseURL>/v<version>/checksums.txt after verifying it against checksums.txt.sigstore.json beside it (sigstore-go): the certificate must be Fulcio's, issued by GitHub's OIDC issuer to the release workflow of Release.SignerRepository (default truvity/sluis) at the tag of that release, with a certificate timestamp and a Rekor entry; the same identity hack/release-verify.sh pins with cosign. A mirror carries the bundle byte for byte. Needs Sigstore's trusted root (TUF) at deploy time |
Release.TrustedRootJSON |
none | A pre-fetched Sigstore trusted_root.json to check the signature against, for a mirrored or air-gapped estate; take it from a verified source, never from the release's own mirror. Empty fetches the public-good root through TUF |
Release.UnverifiedChecksums |
false | Reads checksums.txt without checking its signature: the digest then proves only that the download is what the release page says. For an estate that cannot reach the trusted root; prefer a PackageSHA256 taken from hack/release-verify.sh |
Release.Version |
From the file name | Names the release when Package is empty; (devel) is refused |
Release.BaseURL |
Project GitHub releases | For a mirror |
| Behavior | Rule |
|---|---|
Package empty |
Deploys the library's own release from <BaseURL>/v<version>/sluis-issuer_<version>_linux_arm64.zip (NewBackup and NewRestore: sluis-backup_<version>_linux_arm64.zip). A library at version X expects release X's zips. A pinned digest wins. (devel), pseudo-versions and replaced modules are refused |
| Cache | os.UserCacheDir()/sluis/artifacts; GITHUB_TOKEN goes to github.com |
Audit¶
LambdaArgs.Audit (AuditArgs) chooses where records go; a document naming another adapter is refused.
Audit |
The library |
|---|---|
| Unset | Installs audit beside the function with github.com/truvity/sluis/audit/deploy/pulumi at the same release, named audit-<instance> (Name). The function is the queue's only sender |
Use: &AuditUse{QueueURL, QueueArn} |
Sends to an existing installation. The URL is known when the program runs and in the function's region; QueueArn defaults to the URL's ARN |
Enabled: &false |
Installs nothing and grants no queue; the log adapter writes records to the log only |
| Install input | Meaning |
|---|---|
WriterPackage, WriterPackageSHA256 |
Audit release audit-writer-lambda_<version>_linux_arm64.zip and digest; Guards holds it to the library release. Required |
CatalogueDir |
Directory the release's sluis-audit-catalogue_<version>.tar.gz unpacks to. Required. It travels in the writer's layer, and a catalogue version bump redeploys the writer |
Profiles |
Destination to framework profiles. Default security: [history] (DefaultAuditProfiles), preset operational: writer, archive, deduplication and queue intake, no notary, seal key, alarm or Object Lock. Others need Presets |
DeploymentYAML |
The whole deployment document. Exclusive with Profiles |
Notary, Keys, Alerts, Telemetry, Observe |
Passed through above operational |
Presets, AuditPreset |
One store per preset; never the blob bucket |
Missing inputs are refused before anything is created. A preset store is one of:
| Store | Behavior |
|---|---|
AWS bucket created (Create: true) |
Named <name>-<account>-<region>-<preset> unless Bucket is set; optional Prefix; KeyAlias is looked up, never created. SSE-S3 without Keys.Archive (Archive.Encryption: kms, s3, aws-managed). Object Lock (ObjectLockMode, AcknowledgeCompliance, DefaultRetentionDays) only for the attested preset |
Existing AWS bucket (Create: false) |
Grants only |
S3-compatible (Endpoint) |
No bucket created; Bucket required. Credentials come from the state store at CredentialsAddress (default internal/archive/<preset>), written by an operator, or, with CredentialsRef: external/cloudflare/<preset> and Sluis{Root, KeyArn}, are the R2 credentials a sluis installation rotates, granted as that one parameter. No Object Lock. ReuseBlobStore takes Endpoint, Region, PathStyle from StorageArgs.Blobs; naming the blob bucket is refused |
To adopt an existing audit stack, import it under the new component (resource names).
Keys the estate supplies¶
LambdaArgs.Keys (KeysArgs) takes aliases of symmetric keys you own; the library creates none, and the key policy must admit the function's role. A document naming keys is refused. To adopt library-created keys, supply their aliases and pulumi state delete the old kms.Key resources after unprotecting (steps).
Keys field |
Grant |
|---|---|
Sign (required) |
kms:Encrypt, Decrypt, GenerateDataKey with context {instance: <Instance>, purpose: sign} and no other context key. The runtime wraps ring key pairs locally; no kms:Sign |
Secrets (optional) |
The same actions through SSM only (kms:ViaService ssm.<region>.amazonaws.com), for the installation's parameters (kms:EncryptionContext:PARAMETER_ARN StringLike …:parameter/sluis/<instance>/*). Written as secrets.kmsKeyId, and set as KeyId on the parameters the library creates (the state secret, the recovery password); exclusive with ParameterKeyArn |
| Alias form | alias/<name> of a customer key. An ARN, a key id or alias/aws/… is refused. Re-pointing an alias moves the function at the next apply |
LegacySigningContext (nil is true) |
Also keeps GenerateDataKeyPair and Decrypt on Sign under purpose=sluis-signing for older ring entries (WrappedKeyPolicyStatements belongs in the key policy). Set false after the ring rotates |
RekeyOperatorStatements(region, account, instance, keyArn) |
For the role that runs sluis secrets rekey: ssm:DescribeParameters on *, kms:DescribeKey on the account's keys in the region, ssm:GetParameters and ssm:PutParameter on /sluis/<instance>/*, and the Secrets key's grant above. OperatorAuditStatements(queueArn) adds sqs:SendMessage for its audit record |
SluisKeyPolicyStatements(KeyPolicyArgs) |
The statements of the shared key's policy, each SluisKey… Sid: a statement per function role (its hosted modules' parameters, through SSM), backup read-all and restore write-all, external/* for the External Secrets readers, admin seeds and reads, breakglass, and {instance, purpose: sign} for the roles that host oidc with three denies that hold the purpose to them. No catch-all deny on a use of the key. It ends with the key's control plane: SluisKeyControlPlaneAdmins allows kms:PutKeyPolicy, kms:CreateGrant, kms:ScheduleKeyDeletion, kms:DisableKey, kms:RevokeGrant, kms:CreateAlias, kms:UpdateAlias and kms:DeleteAlias to KeyAdmins and Breakglass, and SluisKeyControlPlaneAdminsOnly denies them to every other principal (AWS services acting on their own excepted). KeyAdmins or Breakglass is required (role ARNs, a * only in the hash of an SSO role's name: CheckKeyAdmin; Admins and Breakglass are held to the same rule); include the role that applies the stack. The estate's own root statement should delegate the upkeep (kms:Describe*, tags, rotation) and no longer kms:*. Concatenate your own statements and pass the lists to KeyPolicyDocument, which refuses a repeated Sid and a policy over 32 KiB |
SluisKey… Sid |
Principals | Grants, through SSM unless said otherwise |
|---|---|---|
SluisKeyRole<Role> |
each function role | Encrypt, Decrypt, GenerateDataKey, DescribeKey on its hosted modules' internal/<module>/* and external/<module>/* |
SluisKeyReadModule<Role> |
a role with ReadModules |
Decrypt of those modules' parameters |
SluisKeySignInClientGoogle |
the google role | Decrypt of the sign-in client under internal/oidc/signin/ |
SluisKeyReadAll<Role>, SluisKeyWriteAll<Role> |
the backup role; the restore role | Decrypt of every internal/* and external/* parameter; the same with Encrypt and GenerateDataKey |
SluisKeyMinter<Role> |
a role with KeyRole.MinterRefs (none today) that does not host cloudflare |
Decrypt of the MinterRefs |
SluisKeyBlobCredentials<Role> |
each role that reads the external blobs' credential (KeyRole.BlobCredentialsRef, from StorageArgs.Blobs.CredentialsRef) and whose own statements do not reach it: on v5 with a stored preset, the issuer and the github, google, outline and slack roles |
Decrypt and DescribeKey of that one parameter |
SluisKeyExternalReaders |
ExternalReaders |
Decrypt of external/* |
SluisKeyAdminSeeds, SluisKeyAdminReads |
Admins, as ARN patterns |
Encrypt and GenerateDataKey on any parameter; Decrypt of the state secret, the recovery password, on v5 the recovery verifier (and the conformance password and verifier when Conformance is on), and, with a backup role on v5, the restore identity |
SluisKeyBreakglass |
Breakglass |
the four actions on any parameter |
SluisKeyDenyRestoreIdentity |
the backup role | Deny Decrypt where the encryption context PARAMETER_ARN is the restore identity |
SluisKeyDenyPasswords |
the roles that host oidc |
Deny Decrypt where PARAMETER_ARN is internal/oidc/recovery-password or conformance-password |
SluisKeySign, SluisKeySignContextReserved, SluisKeySignDirectPurposeOnly, SluisKeySignDirectContextKeysOnly |
the roles that host oidc, directly |
{instance, purpose: sign} and the three denies that hold the purpose to them |
SluisKeyRolesNothingElse |
the same, with DenyOtherActions |
Deny every other action |
Roles before the stack¶
RoleArgs field or method |
Meaning |
|---|---|
| Use | A stack that runs before the sluis stack (the shared key, the audit queue) derives role names without storage, tables or a digest. LambdaArgs and Lambda have the same methods |
Region, AccountID, Instance |
Required |
Installation, Blobs, FunctionName, Modules, BackupFunctionName, RestoreFunctionName, TableNames, TransitionRoles |
As in LambdaArgs |
Backup, Restore |
The stack creates NewBackup, NewRestore; only then is the role Created |
FunctionRoles() |
FunctionRole: Role, Name, Arn, Also, Created, Viewer; Matcher() is the access policy matcher |
Senders(), CreatedSenders() |
Role ARNs: all, or those the queue policy may name because the stack creates them or they already exist |
DeferredSenders(ctx) |
Queue senders as outputs. Call Lambda(l), Backup(b) and Restore(x) after each component, or Reject(err) |
Roles(), ViewerRoles(), ViewerMatchers() |
KeyRole list, and the roles and matchers that read modules' records without hosting them |
KeyPolicyStatements(KeyPolicyOptions) |
SluisKeyPolicyStatements over Roles(). KeyPolicyOptions takes ExternalReaders, Admins, Breakglass, KeyAdmins, DenyOtherActions and ReaderStatements; KeyAdmins or Breakglass is required |
ExternalReaderRole.KeyStatement |
The statement of a role made by NewExternalReaderRole that decrypts its exact addresses. Pass it in ReaderStatements |
The edge modules¶
deploy/pulumi/edge/cloudflare (package edgecloudflare) builds the mutual-TLS custom domain.
Deprecated: use
cloudflare/awsapigw.
store, _ := sluispulumi.NewStorage(ctx, "access", &sluispulumi.StorageArgs{
BucketName: "acme-sluis", Versioning: true, // the domain pins the truststore's version
ProtectedPrefixes: []sluispulumi.ProtectedPrefix{edgecloudflare.Guard(cdRoleArn, operatorsAdminArn, breakglassArn)},
}, pulumi.Providers(awsProvider))
l, _ := sluispulumi.NewLambda(ctx, "access", lambdaArgs, pulumi.Providers(awsProvider))
front, _ := edgecloudflare.NewEdge(ctx, "access", &edgecloudflare.Args{
FrontDoor: l.FrontDoor(),
DomainName: "access.example.test",
CertificateArn: originCertArn, // or Certificate: &edgecloudflare.CertificateArgs{...}
TruststorePEM: cloudflareOriginPullCA,
Storage: store, // or TruststoreBucket: ... for blobs on R2
}, pulumi.Providers(awsProvider))
edgecloudflare.Args |
Meaning |
|---|---|
FrontDoor, DomainName |
Required. The domain takes TLS 1.2 or later |
CertificateArn or Certificate |
One of the two. CertificateArn is a caller-supplied ACM certificate in the function's region. Certificate requests one with DNS validation and calls CreateValidationRecord(ctx, record) (Name, Type, Value) to create the record in your DNS |
TruststorePEM |
Required: CAs a client certificate chains to; for Authenticated Origin Pulls, Cloudflare's origin-pull CA |
Storage or TruststoreBucket |
One of the two |
PublicCache |
Optional; off by default. Builds a cache rule for the public documents and calls CreateCacheRule(ctx, rule) to create it in your zone. TTL defaults to 5 minutes (1 second to 1 hour, whole seconds); Paths defaults to /keys and the two /.well-known metadata documents |
Tags |
On what the edge creates |
Edge output |
Meaning |
|---|---|
DomainTarget, DomainHostedZoneID |
DNS target (CNAME or alias) |
CertificateArn |
Certificate in use |
TruststoreBucketName, TruststoreURI, TruststoreVersion |
Client-CA bundle location and pinned version |
PublicCacheRule |
The rule handed to CreateCacheRule; nil without PublicCache |
| Edge cache | Meaning |
|---|---|
edgecloudflare.PublicCacheRule(host, args) |
The same rule without a Pulumi engine, for a zone managed in another program |
| What it caches | The key set (/keys) and the OpenID and OAuth metadata documents, for requests with no Origin header. The edge TTL overrides the origin, which sends no Cache-Control. A removed key stays served for up to the TTL, hence the one-hour limit |
CacheRule |
Expression and EdgeTTLSeconds. Create it in the zone's http_request_cache_settings ruleset: action set_cache_settings, cache: true, edge_ttl {mode: override_origin, default: EdgeTTLSeconds}, browser_ttl {mode: respect_origin} |
| What it matches | GET and HEAD without an Origin header: the service echoes the caller's origin in Access-Control-Allow-Origin, and Cloudflare ignores Vary: Origin |
| Truststore | Behavior |
|---|---|
| Object | truststore/client-ca.pem in a versioned bucket; the domain pins the version, so a new PEM redeploys |
| Guard | The bucket policy denies write, delete and re-label under truststore/ to everyone except the roles you name (ArnNotEquals on aws:PrincipalArn). Declare it on StorageArgs.ProtectedPrefixes with edgecloudflare.Guard(...). With Storage the edge refuses an unversioned or unguarded bucket |
| Blobs on R2 | Leave Storage unset and give TruststoreBucket (Name, ApplyPrincipalArns): a small protected, versioned, encrypted, private, TLS-only S3 bucket with the same guard |
| Authenticated Origin Pulls | Turn on the zone's tls_client_auth in your own Cloudflare program, else the domain refuses every request. The DNS record for DomainTarget is also yours |
More behavior¶
| Part | Behavior |
|---|---|
| Live alias | Every change publishes a version; alias live points at the newest. API, schedules, scheduler grant, self-invoke grant and the invoke trigger use it. Asynchronous invoke has no retries. Rollback repoints the alias |
| API | HTTP API (payload 2.0), $default route and stage. The execute-api endpoint is off unless KeepDefaultEndpoint. Lambda.FrontDoor() gives a front door the API id, stage and name |
| Schedules | <prefix>-github-<org>, <prefix>-slack-<workspace> and <prefix>-outline-<workspace> invoke {"kind":"tick","controller":"github"\|"slack"\|"outline","target":"<id>"} on the live alias of that module's function; a target is letters, digits and - _ . :, at most 40, with : becoming - in the name. <prefix>-directory-refresh invokes {"kind":"refresh"} on the Google function, <prefix>-cloudflare-rotation invokes {"kind":"cloudflare"} on the Cloudflare function, <prefix>-issuer-clients invokes the issuer. Without module functions every schedule targets the issuer. No retries |
| Scheduler role | <prefix>-scheduler may invoke only the aliases its schedules target. It trusts scheduler.amazonaws.com for this account and the default schedule group: aws:SourceAccount and aws:SourceArn (the group's ARN). The backup function's scheduler role has the same trust |
ExternalReadPolicy(ExternalReadPolicyArgs) |
IAM policy: ssm:GetParameter on exact external/<kind>/<id> ARNs under /sluis/<instance>/; with SecretsKeyArn or ParameterKeyArn, kms:Decrypt through SSM bound to those ARNs. Wildcards, prefixes, repeats and non-external/ addresses are refused |
NewExternalReader(ctx, name, &ExternalReaderArgs{RoleName, Region, AccountID, Instance, Addresses, SecretsKeyArn, ParameterKeyArn}) |
Attaches that policy to the role inline; output PolicyJSON |
| Related | v1.76 upgrade, environment, IAM, signing on AWS, key policy, SSM root |
Upgrades: v1.63, v1.62. Releasing: CONTRIBUTING.