Guide · AWS Secrets Manager

MCP Server Secrets Manager Cross-Account — Resource policy, SecretId ARN reference, PrivateLink

Multi-account MCP deployments — a common architecture for platform teams that run MCP servers in a shared services account while database and API credentials live in workload accounts — require cross-account secret access. Secrets Manager supports cross-account access through a resource-based policy on the secret (similar to S3 bucket policies) combined with IAM identity policy on the consuming principal. This is distinct from role assumption: the MCP server's execution role accesses the secret directly using its own identity, not a role in the secret-owning account. Two complications distinguish cross-account Secrets Manager access from same-account use: the KMS CMK used to encrypt the secret must also be shared via a key policy, and the API call must use the full Secret ARN as the SecretId (not the friendly name).

TL;DR

To share a Secrets Manager secret cross-account: (1) attach a resource policy to the secret granting secretsmanager:GetSecretValue to the consuming account principal, (2) update the KMS key policy to grant kms:Decrypt to the same principal, (3) call GetSecretValue with the full Secret ARN as SecretId (friendly names don't resolve cross-account), (4) optionally route through a Secrets Manager VPC endpoint in the secret-owning account if the consumer VPC needs private connectivity. No role assumption is required.

Resource-based policy on the secret

Secrets Manager resource policies follow the same syntax as S3 bucket policies. The policy is attached to the secret in the owning account and specifies which principals in other accounts are allowed to call which Secrets Manager API actions on that secret.

# Secrets Manager resource policy — attached to the secret in Account A (owner)
# Grants Account B's MCP execution role read access to this secret

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "CrossAccountReadAccess",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::ACCOUNT_B_ID:role/mcp-execution-role"
      },
      "Action": [
        "secretsmanager:GetSecretValue",
        "secretsmanager:DescribeSecret"
      ],
      "Resource": "*"
    }
  ]
}

# AWS CLI — attach the resource policy to the secret
aws secretsmanager put-resource-policy \
  --secret-id "arn:aws:secretsmanager:us-east-1:ACCOUNT_A_ID:secret:mcp/shared/db-creds-AbCdEf" \
  --resource-policy file://cross-account-policy.json \
  --block-public-policy  # Reject policies that would make the secret public

The consuming principal (Account B's MCP execution role) still needs an IAM identity policy that allows secretsmanager:GetSecretValue on the secret ARN. Cross-account access to Secrets Manager requires both the resource policy on the secret (in Account A) and the identity policy on the principal (in Account B). If either is missing, the call fails with AccessDeniedException.

KMS key policy for cross-account decryption

Every Secrets Manager secret is encrypted with a KMS key. By default, Secrets Manager uses an AWS-managed key (aws/secretsmanager) which cannot be shared cross-account. To enable cross-account access, the secret must be encrypted with a customer-managed key (CMK) whose key policy allows the consuming account to call kms:Decrypt.

# KMS key policy addition — add to the existing key policy in Account A
# This allows Account B's MCP execution role to decrypt the secret

{
  "Sid": "AllowCrossAccountDecrypt",
  "Effect": "Allow",
  "Principal": {
    "AWS": "arn:aws:iam::ACCOUNT_B_ID:role/mcp-execution-role"
  },
  "Action": [
    "kms:Decrypt",
    "kms:DescribeKey"
  ],
  "Resource": "*",
  "Condition": {
    "StringEquals": {
      "kms:ViaService": "secretsmanager.us-east-1.amazonaws.com"
    }
  }
}

# Terraform: create the CMK with cross-account access
resource "aws_kms_key" "mcp_secrets" {
  description             = "CMK for cross-account MCP secrets"
  deletion_window_in_days = 30
  enable_key_rotation     = true

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      # Allow Account A full key administration
      {
        Sid       = "AccountAAdminAccess"
        Effect    = "Allow"
        Principal = { AWS = "arn:aws:iam::${var.account_a_id}:root" }
        Action    = "kms:*"
        Resource  = "*"
      },
      # Allow Account B MCP role to decrypt via Secrets Manager only
      {
        Sid       = "CrossAccountDecrypt"
        Effect    = "Allow"
        Principal = { AWS = "arn:aws:iam::${var.account_b_id}:role/mcp-execution-role" }
        Action    = ["kms:Decrypt", "kms:DescribeKey"]
        Resource  = "*"
        Condition = {
          StringEquals = {
            "kms:ViaService" = "secretsmanager.${var.region}.amazonaws.com"
          }
        }
      }
    ]
  })
}

The kms:ViaService condition restricts the cross-account KMS grant to Secrets Manager API calls only — Account B's role cannot use this key grant to decrypt arbitrary data outside of Secrets Manager. This is a defense-in-depth control: even if the IAM principal is later over-permissioned, the key policy prevents misuse of the CMK for non-Secrets Manager decryption.

GetSecretValue with full ARN from the consuming account

Cross-account Secrets Manager calls must use the full Secret ARN as SecretId. Friendly names (e.g., mcp/shared/db-creds) are scoped to the local account's Secrets Manager namespace and cannot resolve cross-account. The full ARN includes the account ID, region, secret name, and a 6-character random suffix generated at secret creation time.

// MCP server in Account B — calling Secrets Manager in Account A
import { SecretsManagerClient, GetSecretValueCommand } from "@aws-sdk/client-secrets-manager";

// The Secrets Manager client is in Account B but the SecretId is in Account A
const sm = new SecretsManagerClient({ region: "us-east-1" });

// IMPORTANT: Use the full ARN, not the friendly name
const SHARED_SECRET_ARN =
  "arn:aws:secretsmanager:us-east-1:ACCOUNT_A_ID:secret:mcp/shared/db-creds-AbCdEf";

async function getSharedCredential() {
  const resp = await sm.send(new GetSecretValueCommand({
    SecretId: SHARED_SECRET_ARN, // full ARN required for cross-account
    // VersionStage defaults to AWSCURRENT — no need to specify
  }));
  return JSON.parse(resp.SecretString);
}

// Common mistake: using the friendly name fails cross-account
// const resp = await sm.send(new GetSecretValueCommand({
//   SecretId: "mcp/shared/db-creds", // WRONG — resolves in Account B, not A
// }));
// → ResourceNotFoundException: Secrets Manager can't find the specified secret

VPC PrivateLink for private cross-account access

If the MCP server runs in a VPC without public internet access, cross-account Secrets Manager calls require a VPC endpoint in the secret-owning account's VPC (or a shared endpoint). The Secrets Manager VPC endpoint (interface endpoint, powered by AWS PrivateLink) creates ENIs in your subnets that route Secrets Manager API traffic privately without leaving the AWS network. For cross-account, the endpoint needs to be in a VPC that is reachable from the MCP server — either through VPC peering, Transit Gateway, or Resource Access Manager (RAM) endpoint sharing.

# Terraform: Secrets Manager VPC interface endpoint in Account A
resource "aws_vpc_endpoint" "secretsmanager" {
  vpc_id              = aws_vpc.shared_services.id
  service_name        = "com.amazonaws.us-east-1.secretsmanager"
  vpc_endpoint_type   = "Interface"
  subnet_ids          = aws_subnet.private[*].id
  security_group_ids  = [aws_security_group.sm_endpoint.id]

  # Enable private DNS: API calls to secretsmanager.amazonaws.com resolve to ENIs
  private_dns_enabled = true
}

# Endpoint policy: restrict which secrets can be accessed through this endpoint
resource "aws_vpc_endpoint_policy" "secretsmanager" {
  vpc_endpoint_id = aws_vpc_endpoint.secretsmanager.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid       = "AllowMCPSharedSecrets"
        Effect    = "Allow"
        Principal = "*"
        Action    = "secretsmanager:GetSecretValue"
        Resource  = "arn:aws:secretsmanager:us-east-1:ACCOUNT_A_ID:secret:mcp/shared/*"
      }
    ]
  })
}

Access pattern comparison

PatternUse whenRequires role assumptionSecretId format
Resource policy + identity policyConsuming role is long-lived; secret is accessed frequently; avoid per-call role assumption latencyNoFull ARN required
AssumeRole into secret-owning accountConsuming role needs broader access in owner account; ExternalId confusion-deputy protection requiredYes (per-call or cached)Friendly name works within assumed session
Replicate secret to consumer accountLow latency is critical; consumer account needs local copy; rotation must propagate to replicasNoLocal ARN in consumer account

Failure modes reference

FailureSymptomFix
Secret encrypted with aws/secretsmanager managed keyGetSecretValue returns AccessDenied; key policy changes have no effectCreate a CMK; re-encrypt the secret with the CMK (must delete and recreate, or use UpdateSecret to change the KMS key)
Using friendly name cross-accountResourceNotFoundException: can't find secret in Account B's namespaceUse the full Secret ARN including account ID and random suffix
Missing kms:Decrypt in consuming role's identity policyAccessDeniedException even though resource policy is correctCross-account KMS requires the principal's identity policy to allow kms:Decrypt — add it explicitly
VPC endpoint private DNS not resolving cross-accountAPI calls time out from consumer VPC; secretsmanager.amazonaws.com resolves to public endpointVerify VPC peering DNS resolution is enabled on both sides; use Route 53 private hosted zone forwarding if DNS doesn't propagate cross-account