Right, so you’ve got a secret in one account and something in another account that desperately needs it. Welcome to the multi-account reality, where we wall things off for security and then immediately have to poke a bunch of carefully controlled holes in those walls to get anything done. It’s the cloud’s version of “we need to have a talk” with your infrastructure.

The first thing to get straight is that neither Secrets Manager nor Parameter Store has a magical “replicate this to Timbuktu” button. AWS would love to sell you a solution that involves Step Functions, EventBridge, Lambda, and a few dozen IAM roles (and honestly, it’s not a terrible idea for complex setups), but for most of us, the goal is something simpler, more robust, and less likely to fail in a way that requires a 3 AM page.

We’re going to focus on the most common and robust pattern: a Lambda function in the destination account that pulls the secret from the source account. This inverts the problem. Instead of the source account needing permission to push secrets everywhere (a security nightmare), the destination account pulls them. The source account just needs to be willing to share. This is a much easier security model to reason about.

The IAM Permissions: The “Can I Have It?” Policy

This is where everyone messes up first. You need two policies: one that lets the puller get the secret, and one that lets it put the secret locally. We’ll tackle the cross-account bit first.

In the SOURCE ACCOUNT, you need to attach a resource-based policy to the secret itself. This policy grants the secretsmanager:GetSecretValue permission to the Lambda function’s role in the destination account. Notice how the principal is the other account’s role. This is the critical handshake.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::DESTINATION_ACCOUNT_ID:role/your-replication-lambda-role"
      },
      "Action": "secretsmanager:GetSecretValue",
      "Resource": "*"
    }
  ]
}

Now, in the DESTINATION ACCOUNT, the execution role for your Lambda function needs two policies. First, it needs permission to assume that it’s allowed to get the remote secret (which the policy above actually allows), and second, it needs permission to write to Secrets Manager in its own account.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "secretsmanager:GetSecretValue",
      "Resource": "arn:aws:secretsmanager:us-east-1:SOURCE_ACCOUNT_ID:secret:my-secret-name-*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "secretsmanager:CreateSecret",
        "secretsmanager:UpdateSecret"
      ],
      "Resource": "arn:aws:secretsmanager:us-west-2:DESTINATION_ACCOUNT_ID:secret:my-replicated-secret-name-*"
    }
  ]
}

The Lambda Function: The Workhorse

Now for the code that does the actual heavy lifting. This Lambda function will be triggered how you see fit—often by an EventBridge rule based on a CloudWatch Event pattern that fires when the source secret is updated. The function’s job is simple: fetch, transform if needed, and store.

import boto3
import json

def lambda_handler(event, context):
    # Define our clients. The get_secret_value call will cross accounts
    # based on the IAM permissions we set up.
    secrets_client_source = boto3.client('secretsmanager', region_name='us-east-1')
    secrets_client_dest = boto3.client('secretsmanager', region_name='us-west-2')

    source_secret_arn = 'arn:aws:secretsmanager:us-east-1:SOURCE_ACCOUNT_ID:secret:my-secret-name'
    dest_secret_arn = 'arn:aws:secretsmanager:us-west-2:DESTINATION_ACCOUNT_ID:secret:my-replicated-secret-name'

    try:
        # Step 1: Get the secret from the source account
        response = secrets_client_source.get_secret_value(SecretId=source_secret_arn)
        secret_value = response['SecretString']

        # (Optional) Step 2: You could modify the secret value here if you needed to.
        # parsed_secret = json.loads(secret_value)
        # parsed_secret['replicated'] = True
        # secret_value = json.dumps(parsed_secret)

        # Step 3: Try to create the secret. If it exists (ResourceExistsException), update it instead.
        try:
            secrets_client_dest.create_secret(
                Name=dest_secret_arn,
                Description='Replicated secret from us-east-1',
                SecretString=secret_value
            )
            print(f"Successfully created secret: {dest_secret_arn}")
        except secrets_client_dest.exceptions.ResourceExistsException:
            secrets_client_dest.update_secret(
                SecretId=dest_secret_arn,
                SecretString=secret_value
            )
            print(f"Successfully updated existing secret: {dest_secret_arn}")

    except Exception as e:
        print(f"Error replicating secret: {e}")
        raise e

    return {
        'statusCode': 200,
        'body': json.dumps('Replication successful!')
    }

The Rough Edges and “What Ifs”

You thought we were done? This is where the real knowledge kicks in.

What about versioning? This code copies the latest version of the secret string. It does not replicate the version history. If you need that, you’re in for a much more complex solution involving describing the secret and iterating through versions. For 99% of use cases, this is overkill. You just care about the current value.

The naming paradox. I used the full ARN for the destination secret name in the create_secret call. This is technically correct, but it’s also a bit weird because the Name parameter usually just takes the friendly name. Using the ARN works because the API is just looking for the secret ID at the end of it. It’s a quirk, but it makes the code clearer. You could just use 'my-replicated-secret-name'.

Error handling is your job. The example above is simplistic. In production, you’d want more robust retries, logging, and maybe even a dead-letter queue to capture secrets that fail replication so you’re not just blindly losing update events. Remember, if this Lambda fails, your secrets are out of sync. That’s a big deal.

What about Parameters? For Systems Manager Parameter Store, the principle is identical, but the API calls change (get_parameter and put_parameter). The huge advantage with Parameters is the ability to use a SecureString, which is encrypted by KMS. And that brings us to the final boss: KMS.

The KMS Problem. If your secret in the source account is encrypted with a custom KMS key (not the default aws/secretsmanager key), you have another cross-account permission to configure. The Lambda function’s role needs to be granted permission to use that KMS key for decryption in the source account. Otherwise, the GetSecretValue call will fail with a cryptic access denied. It’s the number one thing people forget. The same goes for the destination if you’re using a custom KMS key there for the new secret. IAM is the backbone of everything in AWS, and it will always, always be the thing that breaks first.