# 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:

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
-
Open Secrets Manager in
REGIONand choose Store a new secret.
-
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.
-
Switch to the Plaintext tab and confirm the value is a flat JSON object. Nested objects
are not read — only top-level string values.
-
Choose Next and give the secret a name —
SECRETNAME. A path-style name keeps things
tidy when one account serves several nodes.
-
Choose Next. Leave Automatic rotation off for now; see
Rotation and caching.
-
Choose Next, review, and choose Store.


-
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.
-
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.
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. Usefile://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
-
Open IAM > Roles and select your Lambda@Edge Execution Role (
EXECUTIONROLEARN). -
Choose Add permissions > Create inline policy.

-
Switch the policy editor to JSON and paste the policy below, replacing
SECRETARN.
-
Choose Next, name it
DarwiniumEdgeReadSecrets, and choose Create policy.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DarwiniumEdgeReadSecrets",
"Effect": "Allow",
"Action": [
"secretsmanager:GetSecretValue"
],
"Resource": [
"SECRETARN"
]
}
]
}
[!NOTE]
GetSecretValueis 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 needskms:Decrypton 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
- Go to Your Name > Nodes (or Admin > Nodes), open your node, and choose
Edit Node Details. - Open the Edge Deployment tab and select your deployment target on the left.
- Scroll to Secret Management and choose Add store.
- Store name — a label for this store. Free text, unique within the target.
- Store ARN — paste
SECRETARN. - 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.
- Darwinium Secret Name — what you will type inside
- Choose Update.

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_, soapi-keyandapi_keywould both resolve to
dwn_secret_api_keyand 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 |
No — content is a literal constant |
[!WARNING]
Usetemplate:, nevercontent:, when a secret is involved.contentis 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:
- 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. - 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]
KeyValuePairsis markedNoEcho, 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, orput-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