AWS Pulumi library reference¶
The inputs, outputs, resources, IAM and alarms of github.com/truvity/sluis/audit/deploy/pulumi. Mocks test it (just audit-pulumi-test); it has not run in an account (capabilities). See AWS Lambda and the tutorial.
The AWS provider¶
Every invoke and every resource takes the provider passed to New, not the default, which a stack may disable (pulumi:disable-default-providers).
prov, _ := aws.NewProvider(ctx, "audit-account", &aws.ProviderArgs{Region: pulumi.String("eu-west-1")})
a, err := auditpulumi.New(ctx, "audit", args, pulumi.Provider(prov)) // or pulumi.Providers(prov)
| Invoke | When |
|---|---|
kms.LookupAlias |
Once per key named in Keys |
aws.GetCallerIdentity, aws.GetRegion |
Only for SSM grants (Writer.Secrets, or a state store). Args.AccountID and Args.Region skip them |
s3.GetObject |
The catalogue guard, when the writer is given catalogues. Guards.SkipCatalogueCheck skips it |
Optional parts¶
Ingest.Disabled |
Notary.Disabled |
Both | |
|---|---|---|---|
| Left out | Queue and DLQ, DynamoDB table, writer function, role, log group, event source mapping, the writer's and queue's alarms (5) | Notary function, role, log group, schedule and its role, the notary's alarms (3) | All of it |
| Empty outputs | QueueURL, QueueArn, DlqURL, DlqArn, DedupeTableName, WriterFunctionArn, WriterRoleArn |
SealKeyArn, SealKeyAlias, NotaryFunctionArn, NotaryRoleArn, ScheduleArn |
And AlarmTopicArn |
| No longer required | Writer.Package, Writer.PackageSHA256, Writer.DeploymentYAML |
Notary.Package, Notary.PackageSHA256 |
| Rule | Detail |
|---|---|
| Alarm topic | Exists when at least one alarm does |
| Resource counts | options_test.go pins them for its fixture (one created standard bucket, telemetry, alerts, observe), the component included: both parts 43, ingest only 30, notary only 25, neither 10 |
| Use | Ingest without the notary where seals are made elsewhere (for example a notary on Talos signing with OpenBao transit). Neither where both are deferred |
| Turning a part off later | A plain removal on the next pulumi up. The library creates no KMS key, and the archive bucket, which is protected, stays |
Encryption¶
Archive.Encryption has three modes; Archive.KeyArn refines kms.
| Mode | Bucket encryption | Key | Role grants | Functions' kmsKey |
ArchiveKeyArn |
|---|---|---|---|---|---|
kms (default) |
SSE-KMS, bucket keys on | The estate's, named by Keys.Archive (an alias, looked up). The library creates none |
kms:GenerateDataKey, kms:Decrypt (read role: Decrypt) |
The alias | The key the alias points at |
kms with KeyArn |
SSE-KMS under KeyArn, bucket keys on |
Yours, by ARN, instead of Keys.Archive |
Same, on KeyArn |
KeyArn |
KeyArn |
aws-managed |
SSE-KMS under aws/s3, bucket keys on |
None created | None | None: the bucket default applies | Empty |
s3 |
SSE-S3 (AES256) |
None | None | None | Empty |
| Rule | Detail |
|---|---|
KeyArn |
Refused with aws-managed and s3. Must be a key ARN (arn:<partition>:kms:<region>:<account>:key/<id>), not an alias ARN, which IAM cannot grant on |
| Seal key | A different key; it never changes. Written objects keep their encryption |
| Your own key | The roles get the key through IAM policies, so the key policy must let IAM grant access. The default arn:aws:iam::<account>:root statement does; without it every put and get fails with AccessDenied. The library neither edits nor protects a key it did not create. In another account the key policy must also name the roles |
aws-managed |
S3 uses aws/s3 and decrypts for any account principal holding s3:GetObject, so the bucket's IAM and policy are the only plaintext access control. Use is logged in CloudTrail under the account |
s3 |
Gives up the key policy and the CloudTrail record of each use |
| ISO 27001 A.8.24 | Requires a documented cryptography and key-management policy, not a customer-managed key. AWS-managed keys pass when the policy records who rotates (AWS, yearly), who can use the key and how use is evidenced. Choose kms when the ISMS or a customer contract requires control of the key: policy, rotation, separated key administrators and users, or disabling. Use KeyArn when keys are managed centrally |
Inputs¶
| input | default | meaning |
|---|---|---|
Tags |
none | on every resource that takes tags |
AccountID |
looked up | the account; empty looks it up through the component's provider, see the AWS provider |
RolePath |
/audit/ |
the IAM path of every role the library creates |
LogRetentionDays |
30 | each function's log group |
Presets |
required | the install presets the installation uses, by name (operational, standard, attested), each PresetStorage{Bucket, Prefix, Region, Endpoint, PathStyle, CredentialsAddress, CredentialsRef, CredentialsPreset, KeyAlias, Create, Adopt, AcknowledgeLifecycle}: its own store (install presets, 0068). Bucket is required (PresetBucketName builds a default name); Prefix ends in /; Endpoint is an S3-compatible store (no bucket is created, CredentialsAddress is read from the state store, default internal/archive/<preset>); KeyAlias is looked up, never created; Create makes the AWS bucket (versioned, a lifecycle rule per <Prefix>records/<profile>/, compliance Object Lock for attested); Adopt takes an existing bucket and leaves its lifecycle alone (see Lifecycle); with neither, the bucket is used as it is. Every profile of Writer.DeploymentYAML must be kept under a preset configured here, and the library renders the final deployment document with them. Notary, seal key, alarms and pseudonym keys are provisioned when any configured preset needs them: under operational alone there are none, and Notary.Package and Alerts.EndpointURL are refused |
PresetStorage.CredentialsRef |
empty | external/cloudflare/<preset>: the R2 credentials a sluis installation rotates, read below Sluis.Root. The roles get ssm:GetParameter on exactly that parameter and nothing of a minter. Exclusive with CredentialsAddress and CredentialsPreset; with Endpoint only; the mode to prefer on R2 |
AcknowledgeMinterCustody |
false | required with any PresetStorage.CredentialsPreset: the statement that every role that opens the archive holds the Cloudflare minter. Without it the library refuses, naming the preset and pointing at CredentialsRef |
PresetStorage.CredentialsPreset |
nil | mint R2 credentials from a disabled Cloudflare prototype; refused unless AcknowledgeMinterCustody is true. The roles get read on exactly the minter address and read and write on exactly cloudflare-minted/<preset>, so every role that opens the archive holds the minter |
Sluis |
nil | SluisArgs{Root, KeyArn}: the sluis installation's SSM root (/sluis/<instance>) and the key its SecureStrings use (empty is alias/aws/ssm). Required with a CredentialsRef, refused without one; writes archive.sluisRoot and grants kms:Decrypt on KeyArn through SSM only |
Archive.BreakglassRoleArns |
required when the library creates or adopts a bucket; refused when it does not | the roles that alone may bypass the lock: the bucket's policy (LockBreakglassOnly) denies s3:BypassGovernanceRetention, s3:DeleteObjectVersion and s3:PutBucketObjectLockConfiguration to every other principal. Role ARNs (a * only in the hash of an SSO role's name); include the role that applies the stack |
Archive.ObjectLockMode |
COMPLIANCE |
the lock of the attested preset's created bucket alone: GOVERNANCE (the trial) or COMPLIANCE; refused when no attested preset has Create. See the lock modes |
Archive.AcknowledgeCompliance |
false | the deliberate step before COMPLIANCE; without it the library builds nothing |
Archive.DefaultRetentionDays |
required (> 0) with GOVERNANCE and COMPLIANCE |
the bucket's default retention, a floor: the writer sets each object's own. 0 is refused with a lock (a lock with no default rule is a trap), and any value with NONE |
Archive.Encryption |
kms |
kms (SSE-KMS under an archive key), aws-managed (SSE-KMS under aws/s3) or s3 (SSE-S3); the last two create no key and grant no kms on one; see encryption |
Archive.KeyArn |
empty | an existing KMS key ARN for Encryption: kms: no key is created and the roles are granted it; refused with the other modes |
Archive.GlacierIRDays, .DeepArchiveDays |
30, 365 | 0065 |
Region |
looked up | the region, for the ARN of the SSM parameters; looked up like AccountID, and only when there are secrets to grant |
Writer.Secrets.Root, .KeyArn |
/audit/<name>/private/config, none |
where the writer reads the secrets Writer.Keys names, and the customer-managed key they are encrypted with; see secrets. Unset and Writer.Keys naming no secret: no SSM access at all |
Ingest.Disabled |
false | leaves out the queue, the table, the writer and their alarms; see optional parts |
Ingest.Senders |
required unless Ingest.AnySenderInAccount |
principals (role or user ARNs) allowed to send to the queue. The queue policy allows them sqs:SendMessage and denies every other principal: the queue carries no verified caller identity, so this list is the writer's authenticity on this path (authn) |
Ingest.Redrivers |
none | role or user ARNs (an operator's break-glass role) allowed to send to the queue so that a DLQ redrive (StartMessageMoveTask, which sends as the caller) works. Allowed and excepted from the deny, and not a sender the trail relies on; same forms as Senders. See redrive the ingest DLQ |
Ingest.AnySenderInAccount |
false | the acknowledged alternative, for a trial: no sender statement and no deny, so any principal of the account with sqs:SendMessage in its identity policy may send. Refused with Senders |
Ingest.MaxReceiveCount |
5 | deliveries before a message moves to the DLQ |
Ingest.RetentionDays |
14 | the queue's retention; 14 is SQS's limit and the deduplication window's floor |
Writer.Package |
required unless Ingest.Disabled |
the release's audit-writer-lambda_<version>_linux_arm64.zip, a path or an https URL; the function's code as released |
Writer.PackageSHA256 |
required with the package | that zip's SHA-256 in hex, from the release's checksums.txt |
Writer.DeploymentYAML |
required unless Ingest.Disabled |
the profile configuration, the document the chart renders |
Writer.Catalogues |
none | application catalogues by file name (catalogue.yaml, catalogue-<name>.yaml) and content; see the application's catalogue |
Writer.CataloguePaths |
none | the same, read from files on disk under their base names; merged with Catalogues |
Writer.CatalogueSchemas |
none | the data schemas each catalogue references, by the catalogue's file name, then <name>.json and content; a referenced schema missing or a schema unreferenced is refused |
Writer.CatalogueDirs |
none | directories each holding one catalogue document and its .json schemas; merged into Catalogues and CatalogueSchemas |
Writer.Keys, .ForgetIdentities |
none | the keys block and forgetIdentities of the file |
Writer.DedupeWindow |
the profiles' widest | a Go duration |
Writer.MemoryMB, .TimeoutSeconds |
512, 120 | |
Writer.BatchSize |
10 | 1 to 10, the sink's limit |
Writer.MaxBatchingWindowSeconds |
5 | how long the mapping gathers a batch: fewer, larger objects for a few seconds of latency |
Writer.MaxConcurrency |
10 | the mapping's concurrency cap, 2 or more |
Notary.Disabled |
false | leaves out the notary function, its role, log group, schedule and alarms; Keys.Seal must then be unset |
Notary.Package |
required unless Notary.Disabled |
the release's audit-notary-lambda_<version>_linux_arm64.zip |
Notary.PackageSHA256 |
required with the package | its SHA-256 in hex |
Guards.AllowVersionSkew, .SkipCatalogueCheck |
false | acknowledge a binary of another release than the library, and skip the comparison of catalogues with the archive; see guards |
Notary.Schedule |
cron(15 * * * ? *) |
EventBridge Scheduler, UTC |
Notary.Profiles, .Settle |
every profile, 10m |
as audit-notary |
Notary.MemoryMB, .TimeoutSeconds |
256, 900 | |
Telemetry |
nil | nil gives the functions no extension, no OTEL_* and no sts:GetWebIdentityToken |
Telemetry.ExtensionLayerArn |
required with Telemetry |
the OTLP extension, published as a layer in the account and region (by convention audit-otlp) |
Telemetry.OmitLegacyEnv |
false | leave out the deprecated ACCESS_ROSTER_* names of the extension's settings, which are set beside the SLUIS_* ones until v1.76 |
Telemetry.IssuerURL, .OTLPEndpoint |
required with Telemetry |
the issuer's base URL, and the OTLP/HTTP base URL (https) |
Telemetry.STSAudience, .OTLPAudience |
otlp |
the audience asked of STS, which the roles' policies pin, and the exchange's audience |
Telemetry.ExtraEnv |
none | other OTEL_* variables |
Alerts.EndpointURL |
none | the HTTPS endpoint of alert-ingress; none creates the topic and the alarms and no subscription |
Alerts.OldestMessageAgeSeconds |
900 | |
Alerts.NotarySilenceHours |
3 | |
Observe.TrustedPrincipalArn |
one of this, IRSA and PodIdentity is required with Observe |
one IAM role or user that may assume the read role. The account's root, a wildcard and a session ARN are refused when the trust is built |
Observe.ExternalID |
none | required of the assuming principal when set, and required when the principal is in another account than the archive's (AccountID, looked up when empty); does not apply to IRSA |
Observe.IRSA |
none | a ServiceAccount that may assume the read role by web identity, see IRSA |
Observe.PodIdentity |
none | ClusterName, ClusterArn, Namespace, ServiceAccount (all required), Region, PermissionsBoundaryArn: Namespace and ServiceAccount are Kubernetes names (DNS-1123, at most 63 and 253 characters). The role also trusts EKS Pod Identity (pods.eks.amazonaws.com, pinned to the cluster, its account and the ServiceAccount) and the library creates the aws.eks.PodIdentityAssociation. Refused together with IRSA; may be given with TrustedPrincipalArn |
Query |
nil | PodIdentity (the same block, required) and RecordReads (false): a role for audit-query on EKS (its ServiceAccount must differ from Observe.PodIdentity's), <name>-query, with the observe reader's read rights, plus sqs:SendMessage on the ingest queue with RecordReads (refused with Ingest.Disabled) |
ArchiveWriter |
nil | IRSA (the same block) and Prefixes (default seals/, keys/): a write role for a workload outside AWS |
Artifacts bucket and the library's own release¶
With Artifacts, the verified zip goes unchanged (a file asset) to a versioned S3 bucket, and the function and layer come from that object version. Without it, the code uploads with the function.
| Input | Default | Meaning |
|---|---|---|
Artifacts.Bucket |
unset (direct upload) | The artifacts bucket. It must be versioned: an unversioned bucket returns no version id and the apply fails with a message saying so |
Artifacts.Prefix |
audit/ |
Starts every key: <prefix><version>/<sha256>-<file name>. The digest in the key means a key never holds two contents and a re-run uploads nothing new |
Release.ResolveChecksums |
false | Reads an empty Writer.PackageSHA256 or Notary.PackageSHA256 from <BaseURL>/v<version>/checksums.txt after verifying it against checksums.txt.sigstore.json (release workflow of Release.SignerRepository, default truvity/sluis, at the release's tag; see the sluis library). A given digest is used as is |
Release.UnverifiedChecksums |
false | Reads checksums.txt without its signature; see the sluis library's table |
Release.TrustedRootJSON |
none | A Sigstore trusted root for an estate that cannot reach the TUF repository; see the sluis library's table |
CodeSigningConfigArn |
none | A Lambda code signing configuration (artifact.NewCodeSigningConfig) the writer and the notary are put under |
Release.Version |
from the file name | The release when the name does not say; names the release when Writer.Package or Notary.Package is empty. (devel) and empty are refused |
Release.BaseURL |
the project's GitHub releases | Where the release is published, for a mirror |
| Behaviour | Detail |
|---|---|
| Function fields | S3Bucket, S3Key, S3ObjectVersion, SourceCodeHash (the zip's SHA-256, base64) |
| Configuration layer | A zip with identical bytes on every run (sorted names, no timestamps), uploaded under the same prefix |
| No package named | The library deploys its own release: its module version from the build information (or Release.Version), fetched from <BaseURL>/v<version>/audit-<writer\|notary>-lambda_<version>_linux_arm64.zip, digest from checksums.txt unless PackageSHA256 pins one. A pinned digest always wins and mismatching bytes are refused |
| Refused as a release | (devel), a pseudo-version, a module replaced by a local copy, a program without build information. The message names Writer.Package or Notary.Package and Release.Version |
| Download cache | By SHA-256 under os.UserCacheDir()/sluis/artifacts. GITHUB_TOKEN, when set, is sent to github.com |
| After deploy | WriterCodeSha256Matches and NotaryCodeSha256Matches are true when Lambda's code SHA-256 equals the verified zip's |
The live alias¶
| Item | Detail |
|---|---|
| Versions | Each function publishes one per code or configuration change. Alias live points at the newest |
| Users of the alias | The writer's event source mapping, the notary's schedule, the scheduler role's invoke grant (the alias ARN alone, not :*), the notary's asynchronous-invoke configuration (no retries) |
| Rollback | The previous version stays |
| Outputs | WriterLiveAliasArn, NotaryLiveAliasArn, WriterLiveVersion, NotaryLiveVersion |
| Rollout | In one step; CodeDeploy canaries are planned |
Outputs¶
| output | what |
|---|---|
BucketName, BucketArn |
the archive |
ArchiveKeyArn |
the symmetric key objects are encrypted with: the estate's Keys.Archive, or the given Archive.KeyArn; empty with Encryption: s3 or aws-managed |
ArchiveCredentialsPaths |
by preset at an endpoint, the SSM parameter its credentials are read from: the static pair to write by hand, the minter (CredentialsPreset), or the parameter sluis rotates (CredentialsRef, never written by hand); none on AWS S3 |
SealKeyArn, SealKeyAlias |
the estate's ECC_NIST_P384 SIGN_VERIFY key (Keys.Seal) and its alias. audit key public reads its public half for keys/roots.jwks and the verifier's pin |
QueueURL, QueueArn |
the ingest queue a receiver or an application sends to (forward.sqs.queueUrl), and what the chart's sink.sqs of the query service and the jobs names when the writer runs here (observe and query in Kubernetes): QueueURL is the queueUrl, QueueArn the resource of sqs:SendMessage |
DlqURL, DlqArn |
the dead-letter queue |
DedupeTableName |
the DynamoDB table |
WriterFunctionArn, NotaryFunctionArn |
the functions |
WriterRoleArn, NotaryRoleArn, ObserveReaderRoleArn |
the roles, see below; ObserveReaderRoleArn is empty without Observe; the outputs of a part that is turned off are empty too |
ArchiveWriterRoleArn |
the IRSA write role, empty without ArchiveWriter |
QueryRoleArn |
the Pod Identity role of audit-query, empty without Query |
SecretsRoot |
the SSM parameter path the writer reads secrets from, empty when its configuration names none |
AlarmTopicArn |
the SNS topic every alarm publishes to |
ScheduleArn |
the notary's schedule |
What it creates¶
| Resource | Notes |
|---|---|
| S3 bucket | Versioning on and stack-destroy protection in every mode. objectLockEnabled is never set (it forces replacement). Object Lock is a separate resource unless the mode is NONE. SSE-KMS under the archive key with bucket keys (AWS-managed key with aws-managed, SSE-S3 with s3). All four public-access blocks, bucket-owner-enforced ownership, a policy denying plain HTTP, a lifecycle rule per profile prefix, and one aborting incomplete multipart uploads after 7 days. ForceDestroy is never set |
| Scope | The aws partition and one region per stack |
| KMS keys | None. The archive, seal, pseudonym and conceal keys are the estate's, named by alias in Keys (below) |
| SQS ingest queue and DLQ | SSE-SQS; visibility timeout six times the writer's timeout; redrive policy to the DLQ and redrive-allow policy on it; a queue policy denying plain HTTP and allowing the named senders |
DynamoDB <name>-dedupe |
On-demand, hash key pk (string), TTL on expires_at |
Lambda <name>-writer, <name>-notary |
provided.al2023, arm64, no VPC, a log group each, the extension layer when there is one |
| Event source mapping | Queue to writer, ReportBatchItemFailures, scaling capped by Writer.MaxConcurrency |
Scheduler <name>-notary |
The schedule, a role that may invoke only the notary, no retries, no asynchronous retries on the function |
| IAM roles | Below |
CloudWatch alarms, SNS topic <name>-alarms |
Below |
IAM roles¶
Each function has one role under /audit/, named <name>-<part>. For the default installation audit:
| Role | ARN | Runs as |
|---|---|---|
| writer | arn:aws:iam::<account>:role/audit/audit-writer |
The writer function; the identity the OTLP door sees |
| notary | arn:aws:iam::<account>:role/audit/audit-notary |
The notary function; the identity the OTLP door sees |
| observe reader | arn:aws:iam::<account>:role/audit/audit-observe-reader |
Observe in another account or a Kubernetes ServiceAccount, by IRSA or Pod Identity (only with Observe) |
| query | arn:aws:iam::<account>:role/audit/audit-query |
audit-query on EKS, by Pod Identity (only with Query) |
| archive writer | arn:aws:iam::<account>:role/audit/audit-archive-writer |
A Kubernetes ServiceAccount, by IRSA (only with ArchiveWriter); see Kubernetes workloads |
| scheduler | arn:aws:iam::<account>:role/audit/audit-scheduler |
EventBridge Scheduler, to invoke the notary only |
The writer, notary and scheduler roles exist only with their part. The writer and notary never share a role (0061).
| writer | notary | observe reader | |
|---|---|---|---|
| S3 put | PutObject, PutObjectRetention, PutObjectLegalHold on records/, catalogue/, schema/, identity/, dlq/ |
PutObject, PutObjectRetention on seals/, keys/ |
none |
| S3 read | GetObject on the same and holds/; ListBucket |
GetObject on records/, seals/, keys/; ListBucket |
GetObject on records/, catalogue/, schema/, seals/, keys/; ListBucket under those |
| KMS | GenerateDataKey, Decrypt on the archive key |
The same, and Sign, GetPublicKey, DescribeKey on the seal key |
Decrypt on the archive key |
| DynamoDB | GetItem, BatchGetItem, PutItem on the dedupe table |
none | none |
| SQS | ReceiveMessage, DeleteMessage, GetQueueAttributes, ChangeMessageVisibility on the ingest queue |
none | none |
| Logs | Its own log group | Its own log group | none |
| STS | GetWebIdentityToken with Telemetry |
Same | none |
| Rule | Detail |
|---|---|
| Object Lock permissions | S3 refuses a put carrying an Object Lock header unless the caller holds PutObjectRetention and PutObjectLegalHold. The writer needs the legal hold: it places one on the put of a record whose tenant is held. With ObjectLockMode: NONE the functions send no header and both grants are omitted |
| Records are written once | The writer's put into records/ is allowed with the condition s3:if-none-match: * (the store sends If-None-Match: * on every put), so the writer, or a role with its credentials, can add a record and never replace one, in a bucket without Object Lock too. The other prefixes it writes have no condition |
| The scheduler's trust | audit-scheduler is assumed by EventBridge Scheduler for the account (aws:SourceAccount) and its default schedule group (aws:SourceArn …:schedule-group/default: EventBridge Scheduler presents the group's ARN, not the schedule's); the region and account are looked up when Region and AccountID are empty |
| Deletes | No role has one |
| KMS rows | Empty with Encryption: s3 or aws-managed |
| Seal key policy | The account root administers the key (create, describe, enable, put policy, schedule deletion) and cannot use it. Only the notary's role may Sign, GetPublicKey, DescribeKey. The default key policy would let any principal with a kms:Sign allow sign seals |
The web identity statement, for both functions:
{
"Effect": "Allow",
"Action": "sts:GetWebIdentityToken",
"Resource": "*",
"Condition": {
"ForAllValues:StringEquals": { "sts:IdentityTokenAudience": ["otlp"] },
"StringEquals": { "sts:SigningAlgorithm": "ES384" },
"NumericLessThanEquals": { "sts:DurationSeconds": "300" }
}
}
| Element | Reason |
|---|---|
| Audience | From Telemetry (default otlp) |
ForAllValues:StringEquals |
sts:IdentityTokenAudience is multi-valued; a plain StringEquals is an implicit deny on a list. An empty set also passes, which is safe because Audience is required |
| ES384, 300 s | What the extension asks for |
Alarms¶
Alarms publish on ALARM and OK to the SNS topic <name>-alarms, subscribed to alert-ingress over HTTPS. Ingest.Disabled removes the writer's and queue's alarms; Notary.Disabled removes the notary's three.
| Alarm | Metric | Fires when | Means |
|---|---|---|---|
<name>-writer-throttles |
AWS/Lambda Throttles, writer |
Any, in 5 minutes | The writer is not keeping up, or the account's concurrency is spent |
<name>-notary-throttles |
AWS/Lambda Throttles, notary |
Any, in 5 minutes | |
<name>-ingest-dlq-not-empty |
AWS/SQS ApproximateNumberOfMessagesVisible, DLQ |
Above 0 | A record was delivered MaxReceiveCount times and is not in the archive |
<name>-ingest-oldest-message-age |
AWS/SQS ApproximateAgeOfOldestMessage, ingest |
Above Alerts.OldestMessageAgeSeconds (900) |
The writer is behind or not running |
<name>-writer-errors |
AWS/Lambda Errors, writer |
Any, in 5 minutes | An invocation failed |
<name>-writer-unknown-catalogue |
Audit/<name> UnknownCatalogueVersion, a metric filter on the writer's log field event=unknown_catalogue |
Any, in 5 minutes | A record named a catalogue version the writer lacks and was dead-lettered and acknowledged, which neither queue alarm sees. The log line names source and catalogue_version; OTLP carries audit_writer_catalogue_unknown_total |
<name>-notary-errors |
AWS/Lambda Errors, notary |
Any, in an hour | A tenant could not be sealed, or the signer failed |
<name>-notary-silent |
AWS/Lambda Invocations, notary |
Below 1 in each of the last Alerts.NotarySilenceHours (3) hours |
The schedule or function is gone and the seal chain is growing a gap |
| Rule | Detail |
|---|---|
| Notary silence | Alarms on the platform's Invocations metric. Lambda publishes no datapoint for an hour without invocations, so missing data counts as breaching. A stopped function cannot report its own silence |
| Writer silence | Not an alarm: the writer is quiet when nothing is written. The oldest-message-age alarm catches a stopped writer with work waiting |
| Delivery | The subscription has EndpointAutoConfirms false. SNS POSTs a SubscriptionConfirmation and the subscription stays pending until the endpoint follows its SubscribeURL. alert-ingress must handle SubscriptionConfirmation, Notification and UnsubscribeConfirmation and should verify the message signature |
| Topic encryption | None with a customer key, which CloudWatch could not publish to without its own key policy. An alarm body names a queue and a function and carries no record |
Lifecycle¶
One rule per records/<profile>/ prefix:
| Step | When | Note |
|---|---|---|
| Glacier Instant Retrieval | 30 days | Still readable by observe's reindex and audit verify without a restore |
| Deep Archive | 1 year | Reading needs a restore; audit verify over such a range says so first |
seals/, keys/ and catalogue/ have no rule.
An existing bucket whose lifecycle is yours: Adopt¶
| Mode | Behaviour |
|---|---|
Create |
The library owns the bucket and derives the rules above from profile retention, including expiration and noncurrent-version expiration for a profile whose framework deletes at the end of a fixed retention |
| neither | The bucket is outside Pulumi |
Adopt: true |
The library imports the existing bucket by name (never creates it; Protect and RetainOnDelete, so destroying the stack leaves it). It manages versioning, default encryption (per Archive.Encryption), the public-access block, ownership controls (BucketOwnerEnforced) and the bucket policy. The TLS-only deny replaces the bucket's policy as a whole. It declares no lifecycle configuration: existing rules stay as they are, and "never expire" means no expiration rule. Object Lock is untouched |
| Rule | Detail |
|---|---|
First pulumi up |
Imports then changes the resources to these settings; read the preview |
| Refused | Adopt with Create, with Endpoint, or with a bucket another preset names |
| Not applicable | Archive.GlacierIRDays, Archive.DeepArchiveDays and the lock settings; they are refused when no preset has Create |
AcknowledgeLifecycle |
The library cannot read the lifecycle it leaves alone. Adopt is refused when a profile kept in the preset has a fixed minimum retention (security, billing-nl, pci-dss and the like), unless the preset sets AcknowledgeLifecycle: true. That is your statement that the bucket's lifecycle and Object Lock keep objects at least that long |
Not covered¶
| Item | Status |
|---|---|
| A deployment | Nothing here has run in an account |
The lambda sink |
Designed (capabilities); the queue is the transport |
| FIFO ingest | The queue is standard and deduplication is the writer's. A FIFO queue would also absorb a repeat inside its five-minute window and is not what the library creates |
| A signed delegation | The notary signs with a root (0061) |
Keys, state and an S3-compatible archive¶
Args.Keys names keys by KMS alias (alias/<name>), resolved with kms.LookupAlias. An ARN, a key id or an AWS-managed alias is refused.
| Field | Purpose | Required | Role grants |
|---|---|---|---|
Keys.Archive |
archive: SSE-KMS of the objects |
With Encryption: kms unless Archive.KeyArn |
GenerateDataKey, Decrypt (readers: Decrypt); no context condition, S3 binds its own |
Keys.Seal |
seal: the P-384 key seals are signed with |
With the notary; refused without one | Notary: Sign, GetPublicKey, DescribeKey |
Keys.Pseudonym |
pseudonym: wraps the per-tenant secrets |
Never; refused unless the preset is attested or a profile of Writer.DeploymentYAML pseudonymises |
Writer: GenerateDataKey, Decrypt where kms:EncryptionContext:purpose is pseudonym |
Keys.Conceal |
conceal: identities that must be recoverable |
Never; needs Keys.Pseudonym |
Writer: Encrypt, Decrypt, GenerateDataKey where the context is {instance: Keys.Instance, purpose: conceal} |
| Item | Detail |
|---|---|
Keys.Instance |
Default is the component name. It is the instance of the default encryption context of storage/keys and is bound into ciphertexts, so choose it once |
| Function configuration | keys: {adapter: kms, instance, seal, pseudonym, conceal, state} and, for the archive, archive.kmsKey |
SealKeyPolicy(accountRootArn, notaryRoleArn, admins) |
The key policy to put on the seal key; returns an error. The root keeps the key's upkeep (describe, tags, aliases, rotation) and cannot sign; only the notary's role signs, and kms:Sign is denied to every other principal (NoSealButTheNotary). The control plane (kms:PutKeyPolicy, kms:CreateGrant, kms:ScheduleKeyDeletion, kms:DisableKey, kms:RevokeGrant) is allowed to admins and denied to everyone else (AdministerTheKey, ControlPlaneAdminsOnly), so a role with a kms:* allow cannot rewrite the policy and sign. admins (required) are role ARNs, a * only in the hash of an SSO role's name (CheckKeyAdmin). The notary role's ARN is arn:<partition>:iam::<account>:role<RolePath><name>-notary |
NewDigestKey(ctx, name, &DigestKeyArgs{Description, BackupAccountID, KeyAdmins}, opts...) |
The digest signing key (ECC_NIST_P256, SIGN_VERIFY, retained on delete) and its alias, under the caller's options and not a component. Description is verbatim: KMS takes it at creation only. The policy is DigestKeyPolicy(account, admins): the backup account's root enables IAM for kms:Sign, kms:Verify and the key's upkeep, and nobody else is named; the control plane is KeyAdmins' alone (required) and denied to everyone else |
| Digest key hooks | An estate that has the key keeps its URNs and names: KeyLogicalName and AliasLogicalName (default <name> and <name>-alias), AliasName (default alias/<name>), KeyAliases and AliasAliases ([pulumi.Alias]), KeyImport and AliasImport, KeyOptions and AliasOptions |
ArchiveKeyPolicyStatements(ArchiveKeyPolicyArgs) |
The archive purpose on a shared key's policy: AuditReadersOpenTheArchiveThroughS3, AuditRolesUseTheArchiveThroughS3 and OperatorRewritesTheArchiveThroughS3, through S3 for the archive bucket's context only, for Readers, Roles and Operator |
Args.State |
The installation's state store: SSM parameters under Root (default /audit/<name>; KeyArn for a customer-managed key). The archive's credentials are at Root/internal/archive and pseudonym secrets under Root/internal/pseudonym/ |
| Parameters | The library creates none. The writer creates the pseudonym secrets (ciphertext under the pseudonym key) and may create but never replace them |
Preset with Endpoint |
An S3-compatible store: no bucket, lifecycle, encryption setting or S3 or archive-key statement is created. An attested preset there is refused (Object Lock is S3 only). Observe, Query and ArchiveWriter get nothing for it. The writer, and the notary if present, get ssm:GetParameter on the preset's credentials parameter only. See archive on R2 |