Documentation Index

Fetch the complete documentation index at: https://docs.darwinium.com/llms.txt

Use this file to discover all available pages before exploring further.

AWS secret management

Prev Next

# Edge Secrets with AWS Secrets Manager

Store credentials in your own AWS Secrets Manager and use them from a Darwinium journey —
typically a call_url model — so that the credential is never committed to your journeys git
repository.

This guide assumes you have already completed
CloudFront Deployment and your node
deploys successfully. It uses the same terminology and placeholder conventions as that guide
and as S3 Storage Config.

Placeholders used in this guide

Replace these with your own values throughout. The screenshots show one worked example; your
names, ARNs and account will differ.

Placeholder Meaning Where you find it
AWSACCOUNTID Your 12-digit AWS account number AWS Console, top right
REGION Region the secret lives in, e.g. us-east-1 Your choice — see Choosing a region
SECRETNAME Name you give the secret You choose it in Step 1
SECRETARN Full ARN of the secret Output of Step 1
EXECUTIONROLEARN Lambda@Edge Execution Role ARN Portal: Edge Deployment > Deployment Config
DEPLOYMENTROLEARN Deployment Role ARN Portal: Edge Deployment > Deployment Config
EXTERNALID Deployment Config External ID Portal: Edge Deployment > Deployment Config

How it works

Every secret has three names, and keeping them straight is most of the work:

Name Lives in Chosen by
Keystore Name The key inside the AWS secret's JSON value You, in AWS
Darwinium Secret Name The mapping in the Darwinium portal You, in the portal
Template variable Generated internally as dwn_secret_<name> Darwinium

Your journey only ever refers to the Darwinium Secret Name:

url: https://partner.example.com/verify?token={{ secret(call_url_token) }}

So the journey repository holds a reference; the value lives in your AWS account; the mapping
between the two lives in the Darwinium portal.

At build time Darwinium bakes the Store ARN, its region, and the key-name map into the
worker configuration. At request time your Lambda@Edge worker calls GetSecretValue itself and
holds the values in memory. The credential is never stored in the worker bundle, the journeys
repository, or Darwinium's database.

Which credentials are used, and when

Two different AWS identities are involved, and only one of them uses STS and an External ID:

When Identity External ID used? Purpose
Deploy time — pre-flight validation Deployment Role (DEPLOYMENTROLEARN), assumed by Darwinium via STS AssumeRole for 1 hour Yes — the Deployment Config EXTERNALID Reads the secret to check every mapped Keystore Name exists
Request time — worker cold start, then every 10 minutes Lambda@Edge Execution Role (EXECUTIONROLEARN), using the Lambda execution environment's own credentials No. No STS, no External ID Reads the real values

The External ID used for the pre-flight check is the one in the Deployment Config section,
not the one in Monitoring Config. Those are two separate External IDs for two separate
roles.

Both ARNs and the Deployment Config External ID are on the node's Edge Deployment tab:

Darwinium portal Edge Deployment tab: Deployment Config showing the External ID, Distribution ID, Lambda@Edge execution role ARN and Deployment Role ARN, with a separate External ID under Monitoring Config

The practical consequence: EXECUTIONROLEARN is the role that must be able to read the
secret.
Granting the Deployment Role as well is optional but strongly recommended — see
Step 3.


Before you begin

You will need:

  • A working CloudFront (or CloudFront NPM, or AWS Outpost) deployment target.
  • Permission in your AWS account to create a Secrets Manager secret and to grant access to it.
  • Admin access to the node in the Darwinium portal (Your Name > Nodes, or Admin > Nodes).
  • For the CLI steps, AWS CLI v2 authenticated to the same account.

[!NOTE]
The fastest path is Appendix A, a CloudFormation template that creates the secret and
grants both roles access in one step. Steps 1–3 below describe the same thing manually.

Choosing a region

Darwinium reads the region from the Store ARN. Lambda@Edge replicas run in the region
closest to the viewer, so every fetch is a cross-region call back to the secret's home region.
This happens on worker cold start and then only once every 10 minutes — never on the
per-request path — so pick the region closest to the bulk of your traffic and do not worry
about it further.


Step 1 — Create the secret in AWS Secrets Manager

The secret value must be a flat JSON object of key/value pairs. A plain-string secret is
rejected at deploy time.

Using the console

  1. Open Secrets Manager in REGION and choose Store a new secret.

    AWS Secrets Manager Secrets page with the Store a new secret button top right

  2. Choose Other type of secret, then add one row per credential under Key/value. The
    Key is the Keystore Name you will map in the portal in Step 4.

    Choose secret type with Other type of secret selected and two key/value rows entered

  3. Switch to the Plaintext tab and confirm the value is a flat JSON object. Nested objects
    are not read — only top-level string values.

    Plaintext tab showing a flat JSON object with two string values

  4. Choose Next and give the secret a name — SECRETNAME. A path-style name keeps things
    tidy when one account serves several nodes.

    Configure secret step with a path-style secret name entered

  5. Choose Next. Leave Automatic rotation off for now; see
    Rotation and caching.

    Configure rotation step with automatic rotation disabled

  6. Choose Next, review, and choose Store.

    Review step summarising the secret type, name and replication settings

    Success banner confirming the secret was stored

  7. Open the secret and copy the Secret ARN — this is SECRETARN, and you need it in
    Steps 2 and 4. Note the six-character suffix AWS appends; it is part of the ARN.

    Secret details page with the Secret ARN highlighted

  8. Choose Retrieve secret value and check the key names are exactly right. They are matched
    literally and case-sensitively, so a typo shows up as an empty value rather than an error.

    Retrieved secret value listing the secret keys and values

Using the AWS CLI

aws secretsmanager create-secret \
  --region REGION \
  --name SECRETNAME \
  --description "Secrets read by the Darwinium Lambda@Edge worker for call_url steps" \
  --secret-string file://secret.json

where secret.json contains:

{
  "call_url_token": "REPLACE_ME",
  "partner_api_key": "REPLACE_ME"
}

Then capture the ARN and delete the file:

aws secretsmanager describe-secret \
  --region REGION --secret-id SECRETNAME \
  --query ARN --output text

rm secret.json

[!WARNING]
Do not pass credentials with --secret-string '{"k":"v"}' on the command line — it lands in
your shell history and in the process list. Use file:// and delete the file, or read from
your password manager.


Step 2 — Grant the Lambda@Edge Execution Role read access

This is the permission that matters at request time. Without it the worker starts normally, but
every secret(...) renders as an empty string and the only symptom is a CloudWatch log line.

Using the console

  1. Open IAM > Roles and select your Lambda@Edge Execution Role (EXECUTIONROLEARN).

  2. Choose Add permissions > Create inline policy.

    IAM role Permissions tab with the Add permissions menu open showing Create inline policy

  3. Switch the policy editor to JSON and paste the policy below, replacing SECRETARN.

    IAM JSON policy editor containing the DarwiniumEdgeReadSecrets policy

  4. Choose Next, name it DarwiniumEdgeReadSecrets, and choose Create policy.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "DarwiniumEdgeReadSecrets",
            "Effect": "Allow",
            "Action": [
                "secretsmanager:GetSecretValue"
            ],
            "Resource": [
                "SECRETARN"
            ]
        }
    ]
}

[!NOTE]
GetSecretValue is the only action required. Darwinium never writes to the secret and never
lists secrets — Secrets Manager has no API to list the keys inside a secret, so the whole
value is fetched and parsed.

Scope the policy to the specific secret; do not use "Resource": "*". If you want the policy to
survive the secret being recreated, replace the six-character suffix with *, for example
arn:aws:secretsmanager:REGION:AWSACCOUNTID:secret:SECRETNAME-*.

Using the AWS CLI

cat > dwn-edge-secrets-policy.json <<'JSON'
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "DarwiniumEdgeReadSecrets",
            "Effect": "Allow",
            "Action": ["secretsmanager:GetSecretValue"],
            "Resource": ["SECRETARN"]
        }
    ]
}
JSON

aws iam put-role-policy \
  --role-name EXECUTIONROLENAME \
  --policy-name DarwiniumEdgeReadSecrets \
  --policy-document file://dwn-edge-secrets-policy.json

Verify:

aws iam simulate-principal-policy \
  --policy-source-arn EXECUTIONROLEARN \
  --action-names secretsmanager:GetSecretValue \
  --resource-arns SECRETARN \
  --query 'EvaluationResults[0].EvalDecision' --output text
# expected: allowed

If you encrypted the secret with a customer-managed KMS key rather than
aws/secretsmanager, the role also needs kms:Decrypt on that key.


Step 3 — Grant the Deployment Role read access (recommended)

If the Deployment Role can also read the secret, the deploy runs a pre-flight check and fails
with a clear message when a mapped Keystore Name does not exist. If the Deployment Role is
denied, that check is silently skipped
and the deploy succeeds — you then only discover the
problem at request time.

Attach the same policy to DEPLOYMENTROLEARN:

aws iam put-role-policy \
  --role-name DEPLOYMENTROLENAME \
  --policy-name DarwiniumEdgeReadSecrets \
  --policy-document file://dwn-edge-secrets-policy.json

rm dwn-edge-secrets-policy.json

This path uses STS, so the Deployment Role's trust policy must already allow Darwinium to
assume it with EXTERNALID — which it will, if your CloudFront deployment is working. See
CloudFront Deployment Step 7.


Step 4 — Map the secret in the Darwinium portal

  1. Go to Your Name > Nodes (or Admin > Nodes), open your node, and choose
    Edit Node Details.
  2. Open the Edge Deployment tab and select your deployment target on the left.
  3. Scroll to Secret Management and choose Add store.
  4. Store name — a label for this store. Free text, unique within the target.
  5. Store ARN — paste SECRETARN.
  6. Choose Add mapping once per key, and fill in:
    • Darwinium Secret Name — what you will type inside secret(...) in the journey.
    • Keystore Name — the key exactly as it appears in the AWS secret's JSON.
  7. Choose Update.

Secret Management card showing Store name, Store ARN, and two secret mappings of Darwinium Secret Name to Keystore Name

Keeping the two names identical is the least confusing option, but they may differ — useful when
the AWS key name contains characters Darwinium does not allow.

You can add several stores (several ARNs) to one target. Darwinium only fetches the stores that
contain at least one secret your journeys actually use.

Naming rules

Field Allowed Notes
Store name Free text, unique within the target
Store ARN A valid Secrets Manager ARN The region is read from this ARN
Darwinium Secret Name Letters, digits and _ only Must be unique across all stores on the target
Keystore Name Anything AWS permits Must not be empty

[!WARNING]
Use underscores, not dashes, in the Darwinium Secret Name. Dashes are rejected by the portal,
and internally - is folded to _, so api-key and api_key would both resolve to
dwn_secret_api_key and collide.


Step 5 — Use the secret in a journey

secret(NAME) is available inside any Jinja template in a journey step. For a call_url
model, that means:

Field Is it a template?
url Yes — always
request.header_rules[].template Yes
request.body_rule.template (also template_from_file, template_from_url) Yes
request.header_rules[].content and body_rule.content Nocontent is a literal constant

[!WARNING]
Use template:, never content:, when a secret is involved. content is inserted verbatim,
so {{ secret(...) }} would be sent literally to the partner.

Example — token in the query string

- step_name: partner_enrichment
  api_event:
    trigger:
      api_name: ENRICH
  event_type: MiscOther
  models:
    - name: PARTNER_CALL_URL
      dependencies:
        - LOCAL.INPUT
      call_url:
        url: https://partner.example.com/verify?token={{ secret(call_url_token) }}
        method: GET
        request:
          header_rules: []
        response:
          header_rules: []
          body_rule:
            jsonpath:
              - name: $['result']
                extract_to_attribute: custom.general_purpose["partner-result"]

Example — secret in a header and a JSON body

      call_url:
        url: https://partner.example.com/v1/score
        method: POST
        timeout: 4000
        request:
          header_rules:
            - name: Content-Type
              content: application/json
            - name: Authorization
              template: "Bearer {{ secret(partner_api_key) }}"
          body_rule:
            template: |
              {
                "token": "{{ secret(call_url_token) }}",
                "email": "{{ identity[ACCOUNT].email_address }}"
              }
        response:
          header_rules: []
          body_rule:
            jsonpath:
              - name: $['score']
                extract_to_attribute: custom.general_purpose["partner-score"]

Secrets compose with Jinja filters like any other variable, for example
{{ secret(token) | upper }}.

A template that references secret(...) is treated as PII-bearing, exactly as one that
references a PII attribute. For response-side models this moves execution into the
origin-request phase rather than viewer-response. This is expected — CloudFront Functions
cannot make outbound calls at all.


Step 6 — Deploy and verify

Build and deploy the node as usual. During publish, Darwinium checks that:

  1. Every secret(NAME) used in your journeys is mapped in a secret store on that target. This
    check always runs
    and fails the deploy if it does not hold.
  2. Every mapped Keystore Name that is actually used exists in the AWS secret. This runs only
    if the Deployment Role can read the secret
    (Step 3); otherwise it is skipped.

After deploying, send a test request and check the Lambda@Edge logs. Lambda@Edge writes to the
region that served the request, in log groups named /aws/lambda/us-east-1.dwn*. A healthy
worker logs:

tryRefreshAwsSecrets successfully, fetched secrets count = 2

Any of the following means something is wrong — see Troubleshooting:

Failed to get the secret from ARN: <arn>, error: <err>
Key <keystore_name> not found in the secret string for ARN: <arn>
Exception in fetch AWS secret <arn>: <e>

Rotation and caching

  • Values are fetched once at worker cold start, then refreshed every 10 minutes.
  • The refresh runs in the background with a 30-second timeout and does not block requests. A
    failed refresh leaves the previous values in place.
  • Nothing is fetched per request, so Secrets Manager API cost and latency are negligible.

To change a value: update the secret in AWS. No redeploy is needed — every running worker
picks up the new value within 10 minutes, and cold-started workers pick it up immediately.

# put-secret-value REPLACES the whole JSON document, so always send every key.
aws secretsmanager put-secret-value \
  --region REGION --secret-id SECRETNAME \
  --secret-string file://secret.json

Automatic rotation is supported, provided your rotation function preserves the JSON object
shape and the key names. Because the edge caches for up to 10 minutes, the previous value must
remain valid at the partner for at least that long after a rotation — the standard
AWSCURRENT / AWSPREVIOUS two-secret pattern handles this.

[!NOTE]
Adding a new key does require a redeploy, because the key-name map is baked into the
worker configuration at build time. Add the mapping in the portal first, then redeploy.


What ends up where

Artefact Stored in Contains the credential?
Journey YAML Your journeys git repository No — only secret(NAME)
Store ARN, region, key-name map Darwinium, baked into the worker configuration No
Secret store and mappings Darwinium, against the node's deployment target No
The credential itself Your AWS Secrets Manager, your account, your KMS key Yes

Troubleshooting

Symptom Cause Fix
Deploy fails: Secret 'X' is used in journey step '<step>.<model>' but is not defined in any secret store. The journey uses secret(X) but no mapping named X exists on that target Add the mapping in Secret Management, then redeploy
Deploy fails: Secret 'X' not found in AWS Secrets Manager (ARN: ...). Ensure the secret exists and the key name is correct. The Keystore Name does not match a key in the secret's JSON Fix the key name in AWS or in the mapping — matching is literal and case-sensitive
Deploy fails: Secret with ARN '...' value is not a JSON object. Expected a JSON object with key-value pairs. The secret was stored as a plain string Re-store the value as a flat JSON object
Deploy fails: Secret with ARN '...' not found in AWS Secrets Manager. Ensure the secret exists and the ARN is correct. Wrong ARN, wrong account, or wrong region Re-copy the ARN from the secret details page
Deploy fails: Duplicate dwn_key_name: 'X' The same Darwinium Secret Name is mapped in two stores Names must be unique across all stores on the target
Deploy fails: Invalid dwn_key_name 'X', must contain only alphanumeric characters and underscores A dash or other punctuation in the Darwinium Secret Name Use underscores only
Journey error: Invalid secret parameter 'X': only alphanumeric characters, underscores, and dashes are allowed Quotes, spaces or dots inside secret(...) Write secret(api_key) — bare and unquoted
Journey error: Ambiguous Darwinium secret names: 'a-b' and 'a_b', they mapped to same internal name 'dwn_secret_a_b' Dash and underscore variants of one name Standardise on underscores
Deploy succeeds but the outbound request carries an empty value The Execution Role cannot read the secret, and the Deployment Role could not either, so validation was skipped Complete Step 2 and Step 3; check the logs for Failed to get the secret from ARN
Log: Key X not found in the secret string for ARN The key-name mismatch above, caught at request time Fix the key name
Value updated in AWS but the edge still sends the old one The 10-minute refresh window Wait, or redeploy to force cold starts
Access still denied with the policy in place The secret uses a customer-managed KMS key Grant kms:Decrypt on the key to the role

Appendix A — CloudFormation template

darwinium-edge-secret-store.yaml
creates the secret, writes your key/value pairs into it, and grants read access to the
Lambda@Edge Execution Role and, optionally, the Deployment Role. It outputs the Store ARN to
paste into the portal.

Access is granted with a Secrets Manager resource policy naming the two role ARNs. Within a
single account that is sufficient on its own — access is allowed if either an identity policy or
the resource policy permits it — so the template does not need to modify roles that may be
managed by another team or another stack.

Parameters

Parameter Required Description
SecretName Yes SECRETNAME, e.g. myorg/edge/call-url-secrets
KeyValuePairs Yes Comma-separated key=value pairs, e.g. call_url_token=REPLACE_ME,partner_api_key=REPLACE_ME
ExecutionRoleArn Yes EXECUTIONROLEARN from Edge Deployment > Deployment Config
DeploymentRoleArn No DEPLOYMENTROLEARN; enables deploy-time validation
SecretDescription No Free text
KmsKeyId No Customer-managed KMS key; blank uses aws/secretsmanager

Deploy

aws cloudformation deploy \
  --region REGION \
  --stack-name darwinium-edge-secrets \
  --template-file cloudformation/darwinium-edge-secret-store.yaml \
  --capabilities CAPABILITY_IAM \
  --parameter-overrides \
      SecretName=SECRETNAME \
      ExecutionRoleArn=EXECUTIONROLEARN \
      DeploymentRoleArn=DEPLOYMENTROLEARN \
      KeyValuePairs='call_url_token=REPLACE_ME,partner_api_key=REPLACE_ME'

Read back the Store ARN for Step 4:

aws cloudformation describe-stacks \
  --region REGION --stack-name darwinium-edge-secrets \
  --query 'Stacks[0].Outputs[?OutputKey==`SecretArn`].OutputValue' --output text

[!WARNING]
KeyValuePairs is marked NoEcho, so CloudFormation masks it in the console and in
describe-stacks. Even so, passing real credentials as a stack parameter is not ideal. For
production, deploy with placeholder values and then set the real values out of band — the
console's Edit button on the secret, or put-secret-value. Nothing needs redeploying: the
edge picks the new values up within 10 minutes.

[!NOTE]
Values must not contain commas, because the parameter is comma-delimited. Only the first =
in each item is treated as the separator, so values may contain =. For values containing
commas, set them out of band as described above.

If you set KmsKeyId, remember to grant kms:Decrypt on that key to both roles — a Secrets
Manager resource policy cannot grant KMS permissions.

Update or remove

Re-running aws cloudformation deploy with new KeyValuePairs rewrites the secret value.

Deleting the stack deletes the secret with the default 30-day recovery window. Remove the
mapping from the portal and the secret(...) reference from your journeys first, or your
next deploy will fail validation.


Appendix B — Removing a secret manually

# 1. Remove secret() from the journeys and the mapping from the portal, then redeploy.
# 2. Then remove the AWS resources:

aws iam delete-role-policy --role-name EXECUTIONROLENAME --policy-name DarwiniumEdgeReadSecrets
aws iam delete-role-policy --role-name DEPLOYMENTROLENAME --policy-name DarwiniumEdgeReadSecrets

aws secretsmanager delete-secret \
  --region REGION \
  --secret-id SECRETNAME \
  --recovery-window-in-days 7