Skip to content
Rotate AWS Secrets Manager Secrets

Rotate AWS Secrets Manager Secrets

Overview

Synchronization pushes a value Britive already holds. Rotation goes the other way: Britive generates a new value, the broker writes it into AWS Secrets Manager, and Britive keeps the value so it can be vended later.

Two standalone scripts cover the two situations. Each is a single self-contained file — upload it as the rotation script on a resource type and nothing else needs to be installed.

What you’ll accomplish:

  • Pick the script that matches what actually owns the credential
  • Configure the rotation variables in Britive
  • Verify the new value is the one consumers now read

Britive generates the password, not the script. Both scripts require AWS_NEW_PASSWORD and fail immediately without it. A password generated inside the script would exist only in that process — the platform could neither store nor vend it, so the rotated credential would be lost the moment the script exited.

How It Works

    flowchart TD
    Sched["Britive rotation<br/>(schedule or on demand)"]
    Broker["rotate-secret-*.sh<br/>(runs on the broker)"]
    Target["Target account<br/>(database, service account)"]
    SM["AWS Secrets Manager"]

    Sched -->|"injects AWS_NEW_PASSWORD (encrypted) + resource attributes"| Broker
    Broker -->|"1. change the account (with-target only)"| Target
    Broker -->|"2. PutSecretValue"| SM
    Broker -->|"3. read AWSCURRENT back and compare"| SM
  

Which Script

rotate-secret-value.shrotate-secret-with-target.sh
What owns the credentialthe secretthe account behind it
Use forAPI keys, shared tokens, values another process re-registersdatabase logins, service accounts
Secret shapeJSON object or plaintextJSON object only (it needs a username)
Extra inputSECRET_TARGET_HOOK

Rotating only the secret when a real account sits behind it leaves the secret advertising a password the account never accepted. Every consumer then fails to authenticate, and nothing about the secret says why.

For Active Directory, use neither — rotate-ad-account-aws-secret.sh already does both halves natively over LDAPS.

Before You Begin

  • The Access Broker is deployed and connected
  • aws, jq, and python3 on the broker host (all three ship in the britive/bridge image)
  • The broker’s IAM role holds, on the target secret only:
    • secretsmanager:DescribeSecret, secretsmanager:GetSecretValue, secretsmanager:PutSecretValue
    • kms:Decrypt and kms:GenerateDataKey if the secret uses a customer-managed key
  • The secret already exists — a rotation rotates, it never creates

Variables

VariableRequiredDescription
AWS_SECRETYesThe secret’s name or full ARN — either works
AWS_NEW_PASSWORDYesThe new value. Configure it as an encrypted rotation variable; Britive generates it
AWS_REGIONConditionalRequired when AWS_SECRET is a name. A full ARN carries its own region
AWS_SECRET_KEYNoJSON key to patch (default password)
AWS_SECRET_MODENoauto (default), json-key, or plaintext

Modes. auto inspects the current value: a JSON object is patched at AWS_SECRET_KEY with every other field preserved; anything else is replaced wholesale. json-key refuses a plaintext secret rather than overwrite it. plaintext replaces the whole value — destructive on a JSON secret, which is why it must be asked for by name.

Resource-level values (AWS_REGION, AWS_SECRET) can come from the resource’s attributes instead — the broker delivers those as RESOURCE_AWS_REGION and RESOURCE_AWS_SECRET. See How the Broker Passes Values to a Script.


Inside the Script

You don’t need to read the whole file. These four pieces are what matter.

Inputs, read plainly

# Own name first, resource attribute second, so the script also runs by hand.
SECRET_ID="${AWS_SECRET:-${RESOURCE_AWS_SECRET:-}}"
REGION="${AWS_REGION:-${RESOURCE_AWS_REGION:-}}"

[ -n "${AWS_NEW_PASSWORD:-}" ] \
  || die "AWS_NEW_PASSWORD is not set — configure the attribute on the rotation in Britive"

# A full ARN already carries its region; deriving it beats setting it twice.
if [ -z "$REGION" ]; then
  case "$SECRET_ID" in
    arn:*:secretsmanager:*) REGION="$(printf '%s' "$SECRET_ID" | cut -d: -f4)" ;;
  esac
fi

# Take the value out of the environment so nothing this script spawns inherits it.
NEW_VALUE="$AWS_NEW_PASSWORD"
unset AWS_NEW_PASSWORD

Refuse secrets that shouldn’t be rotated here

DescribeSecret runs before anything is written:

[ -z "$DELETED_DATE" ] || die "secret is scheduled for deletion; restore it before rotating"

if [ -n "$OWNING_SERVICE" ] && [ "${ALLOW_SERVICE_OWNED:-false}" != "true" ]; then
  die "secret is owned by ${OWNING_SERVICE}, which rotates it itself"
fi

[ "$NATIVE_ROTATION" != "true" ] \
  || warn "AWS-native rotation is also enabled; the next Lambda run overwrites this value"

A secret created by RDS or Redshift is held by that service too. Writing to it here leaves the service holding a password its own records say is different.

Stage to a file, never to the command line

# jq --arg passes the value as data: no quoting can corrupt it, and every
# other field in the JSON object is preserved.
( umask 077; jq --arg k "$SECRET_KEY" --arg v "$NEW_VALUE" '.[$k] = $v' \
    < "$CURRENT_FILE" > "$STAGED" )
chmod 600 "$STAGED"

aws secretsmanager put-secret-value \
  --secret-id "$SECRET_ID" --region "$REGION" \
  --secret-string "file://${STAGED}" \
  --client-request-token "$REQUEST_TOKEN"

--secret-string on the command line would expose the plaintext in /proc/<pid>/cmdline to everything in the container. The client request token makes a retried call idempotent: if the first attempt succeeded and only the response was lost, the retry returns the same version instead of creating a second one.

Verify by reading it back

aws secretsmanager get-secret-value --secret-id "$SECRET_ID" \
  --version-stage AWSCURRENT --query SecretString --output text > "$VERIFY_FILE"
# compare against the value written; nothing is reported as rotated until it matches

An API 200 alone would miss a staging label that never moved. The point of a rotation is that the new value is the one consumers get.


The Target Hook

rotate-secret-with-target.sh changes the account first, the secret second, and the hook is how it changes the account. The contract is small:

  • The new password arrives on stdin — not on argv (visible in /proc/<pid>/cmdline), not in the environment (inherited by anything the hook spawns)
  • SECRET_ARN, SECRET_NAME, SECRET_USERNAME, and AWS_REGION are in its environment, alongside the RESOURCE_* attributes already in scope
  • Exit 0 means the account accepted the password and it is live now — only then is the secret written
  • Exit non-zero means nothing is written; the hook’s stderr is quoted in the failure
  • It must be idempotent — a rotation retried after a lost response runs it again
set-db-password.sh
#!/bin/bash
set -euo pipefail
read -r NEW_PASSWORD          # stdin, nothing else

mysql --host "$RESOURCE_HOST" --user admin \
  -e "ALTER USER '${SECRET_USERNAME}'@'%' IDENTIFIED BY '${NEW_PASSWORD}';"

The hook runs with the broker’s credentials, so the script refuses one that is world-writable.

If the account changes but the secret write then fails, the script exits non-zero and reports DIVERGED, naming both sides. That is a real break needing a manual fix — it is never reported as success. Read the error: it tells you which side already changed.


Configure in Britive

Add the rotation script

In Resource Manager → Resource Types, open the resource type and paste the script as its rotation routine.

Add the rotation variables

Add AWS_SECRET (or AWS_SECRET_ARN) and any optional variables. Add AWS_NEW_PASSWORD and mark it encrypted — Britive generates the value and injects it decrypted at run time.

Set the schedule

Configure the rotation interval, or leave it on demand.

Run it once

Trigger the rotation manually and read the broker log before trusting a schedule.


Verify

Check the script output

A successful run emits key=value lines — and never the value itself:

secret_arn=arn:aws:secretsmanager:us-west-2:123456789012:secret:app/db-AbCdEf
secret_name=app/db
mode=json-key
secret_key=password
preserved_keys=host, port, username, password
new_version=7f3c...
rotated=true
verified=true

Confirm the other fields survived

aws secretsmanager get-secret-value --secret-id "app/db" \
  --query SecretString --output text | jq 'keys'

username, host, and port are still present — only password changed.

Know where the old value is

PutSecretValue demotes the previous version to AWSPREVIOUS, so rollback needs no backup step of your own:

aws secretsmanager get-secret-value --secret-id "app/db" --version-stage AWSPREVIOUS

Troubleshoot

SymptomCauseFix
AWS_NEW_PASSWORD is not setThe rotation has no such variableAdd AWS_NEW_PASSWORD as an encrypted variable on the rotation
no regionAWS_SECRET is a name and no region was givenPass the full ARN, or set the resource’s region attribute
secret is owned by <service>The secret belongs to RDS, Redshift, etc.Rotate through that service; ALLOW_SERVICE_OWNED=true only if you are certain
is not a JSON objectPlaintext secret, json-key modeUse AWS_SECRET_MODE=plaintext to replace the whole value
DIVERGEDAccount changed, secret write failedRe-run once the secret is writable, or set the account back to the value still in the secret
Value reverts hours laterAWS-native rotation is also enabledDisable the Lambda rotation, or rotate only through AWS
Log shows nothing usefulBritive keeps ~250 characters of outputThe scripts print the reason first and the trace after; set ROTATE_VERBOSE=true for a hand-run

Next Steps

Last updated on