If icon
AwsCLI icon
Return icon
Switch icon
Log icon
SlackIncomingWebhook icon
Fail icon
Schedule icon

KMS Key Rotation Compliance Gate

Audit AWS KMS key rotation status, alert Slack when disabled, and optionally enable rotation with a verified fix.

Categories
CloudInfrastructure

A customer-managed KMS key with automatic rotation disabled keeps encrypting new data under the same key material indefinitely, exactly the setting most security baselines and compliance frameworks require enabled, and nothing about the key itself looks different, encryption and decryption both keep working whether rotation is on or off. get-key-rotation-status already reports the real KeyRotationEnabled value, this blueprint is what reads it on a schedule and turns a disabled key into an alert, with a narrow, single-key remediation path instead of leaving it as a standing finding.

How it works

  1. route_connection_mode (io.kestra.plugin.core.flow.If) picks the AWS CLI endpoint based on use_localstack. Both branches run the identical aws kms get-key-rotation-status --key-id key_id via io.kestra.plugin.aws.cli.AwsCLI, captured as named outputs through the plugin's own documented ::{"outputs":{}}:: echo convention. The command's own JSON response is already a flat object (KeyId, KeyRotationEnabled, and more), so no --query reshaping is needed this time, only the connection differs between branches.
  2. classify_rotation_status (io.kestra.plugin.core.debug.Return) reads whichever branch's vars.KeyRotationEnabled exists (guarded with is defined/is not defined) and returns PROBE_FAILED when neither branch returned at all, COMPLIANT when it is true, CRITICAL or WARNING depending on violation_severity when it is not.
  3. route_by_rotation_status (io.kestra.plugin.core.flow.Switch) branches on that status: COMPLIANT logs it, WARNING, CRITICAL, and PROBE_FAILED each alert Slack.
  4. remediate_gate (io.kestra.plugin.core.flow.If) runs only when the key was not COMPLIANT, auto_remediate is true, and dry_run is false:
    • route_remediation_connection picks the same connection mode again, then calls aws kms enable-key-rotation --key-id key_id on this one key, never a bulk call across every key in the account.
    • verify_rotation_* re-runs get-key-rotation-status, and report_remediation_result (io.kestra.plugin.core.flow.If) fails the flow with io.kestra.plugin.core.execution.Fail if it still does not report true, so a failed enable is never silently reported as fixed.
  5. log_audit_summary always runs last and logs the final classification.
  6. The errors block posts to Slack if the flow itself fails outside the gate logic above.

What you get

  • A real answer to whether a specific KMS key actually has rotation enabled, not an assumption from whoever created it.
  • A Slack alert the moment a key's rotation setting changes to disabled, instead of finding out during a compliance audit months later.
  • A narrow, single-key remediation path that verifies the fix actually took, instead of trusting the enable call's own silence.
  • A fully local test path: the default inputs point at a LocalStack container with no real AWS account needed, both operations this blueprint uses are implemented in LocalStack's community edition.

Who it's for

  • Security and platform teams enforcing that every customer-managed KMS key rotates, a common requirement in SOC 2, PCI DSS, and similar frameworks.
  • Teams who want a standing, scheduled check instead of relying on a point-in-time audit to catch a key someone created, or changed, without rotation enabled.
  • Teams evaluating the AWS CLI plugin who want a working example of the ::{"outputs":{}}:: convention applied to a command whose own JSON output is already a flat object, no --query reshaping required.

Why orchestrate this with Kestra

get-key-rotation-status is a single point-in-time read, KMS itself has no concept of a schedule, a severity policy, or an alert built from what it finds. Kestra adds the recurring check, the severity policy per key, the Slack alert, the single-key gated remediation, and the verification that the fix worked, all as one declarative YAML file you can version and review like code, runnable against LocalStack before it ever touches a real key.

Prerequisites

  • For local testing, a LocalStack container, no AWS account needed:

    docker run --rm -d --name localstack -p 4566:4566 localstack/localstack
    

    Port 4566 does not collide with Kestra's own default UI port. Both GetKeyRotationStatus and EnableKeyRotation are implemented in LocalStack's community edition.

  • For real AWS, an IAM credential with kms:GetKeyRotationStatus permission to read, and additionally kms:EnableKeyRotation if auto_remediate will ever be used.

  • A Slack incoming webhook for alerts.

  • If Kestra itself runs in a container and use_localstack is true, point localstack_endpoint at http://host.docker.internal:4566 or connect Kestra's container to LocalStack's Docker network and use its container name instead, check the exact network name first with docker network ls.

Secrets

  • AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY: used by the *_live tasks only, not needed when use_localstack is true, LocalStack accepts the fixed test/test credentials this blueprint hard-codes on the LocalStack branches.

  • SLACK_WEBHOOK: Slack incoming webhook URL used by the alert tasks and the errors block.

    In Kestra OSS, set these as base64-encoded environment variables, for example SECRET_AWS_ACCESS_KEY_ID, SECRET_SLACK_WEBHOOK.

Inputs

Input Type Default Allowed values
key_id STRING <your-kms-key-id> any customer-managed KMS key ID or ARN
aws_region STRING us-east-1 any AWS region
violation_severity SELECT critical critical, warning
auto_remediate BOOL false true, false
dry_run BOOL true true, false
use_localstack BOOL true true, false
localstack_endpoint STRING http://localhost:4566 any LocalStack endpoint URL

Quick start

  1. Start LocalStack and create a test key without rotation: aws --endpoint-url=http://localhost:4566 kms create-key (note the returned KeyId).
  2. Add the SLACK_WEBHOOK secret.
  3. Set key_id to that key and run this flow manually, it should report CRITICAL.
  4. Set auto_remediate: true and dry_run: false and run it again, it should enable rotation, verify it, and report remediation_confirmed.
  5. Set use_localstack: false and add the AWS secrets to point this at a real key.
  6. Enable the daily trigger.

How to extend

  • Loop this flow over a list of key IDs with io.kestra.plugin.core.flow.Loop to audit every customer-managed key in an account from one flow, instead of one key per run.
  • Add a second probe for describe-key and check KeyManager is CUSTOMER before proceeding, as a safety check against accidentally pointing key_id at an AWS-managed key.
  • Read RotationPeriodInDays and NextRotationDate, also returned by get-key-rotation-status, to alert when a key's rotation period itself was changed to something unexpectedly long, not only when rotation is fully disabled.

Pitfalls

  • AWS-managed keys (aws/s3, aws/rds, and similar) rotate automatically on their own schedule and cannot have rotation disabled or enabled through this API the same way, pointing key_id at one is out of scope for this gate, scope it to customer-managed keys only.
  • enable-key-rotation on a key pending deletion, or in certain other non-Enabled key states, fails. allowFailure: true on enable_rotation_* lets the flow continue to verify_rotation_* either way, which then correctly reports the fix did not take.
  • This blueprint remediates exactly one key per run, the one named in key_id, there is no bulk-enable path, intentionally, enabling rotation account-wide without reviewing which keys are AWS-managed first is not something this flow will do automatically.
  • KeyRotationEnabled only covers automatic rotation. RotateKeyOnDemand, a separate operation, performs a one-time rotation without changing this setting, this gate does not check for or trigger that.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.