Signing on AWS: the key and its policy¶
IAM statements and settings of the kms-wrapped signing ring, and the key policy of the deprecated library-created key. Rationale: Signing on AWS.
The role's grant¶
Keys.Sign (keys.sign) names the key by alias. The function's role, and the Kubernetes role when it signs, get on that key only:
{
"Sid": "SluisKeysSign", "Effect": "Allow",
"Action": ["kms:Encrypt", "kms:Decrypt", "kms:GenerateDataKey"],
"Resource": "<the key's ARN>",
"Condition": {
"StringEquals": {"kms:EncryptionContext:instance": "<instance>", "kms:EncryptionContext:purpose": "sign"},
"ForAllValues:StringEquals": {"kms:EncryptionContextKeys": ["instance", "purpose"]}
}
}
There is no kms:Sign grant: the process wraps its own pairs. instance is the document's instance or release. The library writes no key policy, so the key must admit the role (trust boundary).
Set Keys.LegacySigningContext (nil is true) false once the ring has rotated past entries written before local wrapping.
The secrets key. Keys.Secrets names the key that encrypts the SSM parameters (secrets.kmsKeyId). It grants kms:Encrypt, kms:Decrypt and kms:GenerateDataKey through SSM only, for /sluis/<instance>/* only.
Configuration¶
preset: aws-hybrid
keys:
adapter: kms
sign: alias/sluis-demo-sign # or {key: alias/..., context: off | {..}}
signingKey:
kmsWrapped:
stateSecret: issuer/state-secret
# algorithms: [ES384, RS256] # the first is the default
# rotateEvery: 24h
# prepublish: 15m
# retain: 65m
Keys.Sign writes keys; a document that names keys is refused. signingKey.kmsWrapped.keyId is the deprecated spelling of keys.sign. stateSecret is a secret name the library writes. adapters.signing takes the same settings:. Moving from kms: Upgrade to v1.62, step 6.
Monitoring rotation¶
| Signal | Behavior |
|---|---|
| Issuer log | A failing rotation keeps the old key signing. ERROR at most hourly once the active key is 1.5 times rotateEvery old |
| Metric | sluis_signing_key_active_since_timestamp_seconds; alert past rotateEvery + prepublish + margin (26h by default) |
| Chart rule | SluisSigningKeyRotationStalled. Unset alerts.rules.signingKeyRotationStalled.maxAgeSeconds derives rotateEvery plus 2h from config.signingKey (93600 for 24h), else 350 days |
| Lambda | No chart: load the rule below into vmalert or a PrometheusRule |
- alert: SluisWrappedSigningKeyRotationStalled
expr: time() - max by (algorithm) (sluis_signing_key_active_since_timestamp_seconds) > 93600
for: 15m
labels: {severity: warning}
annotations:
summary: 'The {{ $labels.algorithm }} signing key has been active for {{ $value | humanizeDuration }}: rotation is failing'
Legacy: WrappedSigning, the library creates the key¶
Deprecated. The function's role, and the Kubernetes role when it signs, get on the key only:
{
"Sid": "SluisWrappedSigning", "Effect": "Allow",
"Action": ["kms:GenerateDataKeyPairWithoutPlaintext", "kms:Decrypt"],
"Resource": "<the key's ARN>",
"Condition": {
"StringEquals": {"kms:EncryptionContext:purpose": "sluis-signing"},
"ForAllValues:StringEquals": {"kms:EncryptionContextKeys": ["purpose", "alg", "kid"]}
}
}
The key policy¶
WrappedSigning.KeyArn takes a possibly shared key; merge the statements below into it (sluispulumi.WrappedKeyPolicyStatements(roleArns)). Unset, the library creates a protected symmetric key with automatic rotation (alias WrappedSigning.KeyAlias, required) with this policy. It needs LambdaArgs.KeyAdmins, role ARNs.
| Sid | Effect | Principal | Action | Condition |
|---|---|---|---|---|
EnableIAMPolicies |
Allow | the account root | kms:Decrypt, Encrypt, GenerateDataKey*, ReEncrypt* and the upkeep actions (describe, list, tags, rotation) |
none (IAM policies govern the use of the key) |
SluisKeyControlPlaneAdmins |
Allow | the account root | kms:PutKeyPolicy, CreateGrant, RevokeGrant, DisableKey, ScheduleKeyDeletion, CreateAlias, UpdateAlias, DeleteAlias |
aws:PrincipalArn matches KeyAdmins |
SluisKeyControlPlaneAdminsOnly |
Deny | * |
the same eight | aws:PrincipalArn matches none of KeyAdmins, and the caller is not an AWS service |
SluisSigningContextReserved |
Deny | * |
kms:Decrypt, kms:Encrypt, kms:ReEncrypt*, kms:GenerateDataKey*, kms:CreateGrant |
kms:EncryptionContext:purpose is sluis-signing, and aws:PrincipalArn is not one of the signing roles (the function's role, and WrappedSigning.AdditionalSigningRoleArns, the Kubernetes role) |
SluisSigningRolePurposeOnly |
Deny | * |
kms:GenerateDataKeyPairWithoutPlaintext, kms:Decrypt |
aws:PrincipalArn is a signing role, and kms:EncryptionContext:purpose is not sluis-signing |
SluisSigningRoleContextKeysOnly |
Deny | * |
the same two | a signing role, and ForAnyValue:StringNotEquals kms:EncryptionContextKeys ["purpose","alg","kid"] |
SluisSigningRoleNothingElse |
Deny | * |
NotAction the same two |
a signing role |
The statements to merge¶
[
{
"Sid": "SluisSigningContextReserved", "Effect": "Deny", "Principal": {"AWS": "*"}, "Resource": "*",
"Action": ["kms:Decrypt", "kms:Encrypt", "kms:ReEncrypt*", "kms:GenerateDataKey*", "kms:CreateGrant"],
"Condition": {
"StringEquals": {"kms:EncryptionContext:purpose": "sluis-signing"},
"ArnNotEquals": {"aws:PrincipalArn": ["<the function role's ARN>", "<the Kubernetes role's ARN, if it signs>"]}
}
},
{
"Sid": "SluisSigningRolePurposeOnly", "Effect": "Deny", "Principal": {"AWS": "*"}, "Resource": "*",
"Action": ["kms:GenerateDataKeyPairWithoutPlaintext", "kms:Decrypt"],
"Condition": {
"ArnEquals": {"aws:PrincipalArn": ["<the same signing role ARNs>"]},
"StringNotEquals": {"kms:EncryptionContext:purpose": "sluis-signing"}
}
},
{
"Sid": "SluisSigningRoleContextKeysOnly", "Effect": "Deny", "Principal": {"AWS": "*"}, "Resource": "*",
"Action": ["kms:GenerateDataKeyPairWithoutPlaintext", "kms:Decrypt"],
"Condition": {
"ArnEquals": {"aws:PrincipalArn": ["<the same signing role ARNs>"]},
"ForAnyValue:StringNotEquals": {"kms:EncryptionContextKeys": ["purpose", "alg", "kid"]}
}
},
{
"Sid": "SluisSigningRoleNothingElse", "Effect": "Deny", "Principal": {"AWS": "*"}, "Resource": "*",
"NotAction": ["kms:GenerateDataKeyPairWithoutPlaintext", "kms:Decrypt"],
"Condition": {"ArnEquals": {"aws:PrincipalArn": ["<the same signing role ARNs>"]}}
}
]