Skip to content

What the template creates

The template adds storage, EventBridge rules and one IAM role for our service to the AWS account that holds your data. It contains no code: no Lambda functions, no custom resources, no VPC, and no IAM users or access keys. All of Rhino Relay’s code runs in our AWS account and reaches yours only through the grants on this page.

You can launch it in any Region where Amazon SES sends email: the one you registered with. See Regions and limits.

You pay AWS for what these resources use, on your own AWS bill: SES sending, DynamoDB, S3, EventBridge, Data Firehose, Glue and CloudTrail if you create the trail. Our fee is separate. For a list of 100,000 contacts sent once a month, AWS usage comes to about $13 a month, mostly SES at $0.10 per 1,000 emails.

Every table and policy statement below is read from the released template (infra/tenant-template/released/v1.json) when these docs are built, so it’s exactly what you install. Placeholders stand for values filled in at install time: <region> is the stack’s Region, <your account> is your AWS account ID, <our account> is our service account, and names such as <SendingDomain> are template parameters. <RuleDlq.Arn> means the ARN of the resource with logical ID RuleDlq.

Where the template grants our account access

Section titled “Where the template grants our account access”

Our account appears in exactly these places, and nowhere else:

  1. The access role’s trust policy, for our one service role, rhino-relay-prod-service-<region>, in the stack’s Region, with your external ID required.
  2. The forwarding rules, ForwardRule and UploadsRule, which send SES events from our configuration sets and notifications of CSV uploads to our event bus, rhino-relay-prod-tenants, in the stack’s Region. ForwardRole lets EventBridge put events on that one bus and nothing else.

Setting Disconnect to true removes both: the trust policy stops trusting our account, the access role’s permissions are deleted, and the forwarding rules and their role are deleted. See Disconnecting.

The template doesn’t create any queue our service reads. Our work queues are in our own account, shared by all clients, and carry IDs only. See How it works.

Marketplace fills in the first three for you. The console groups the parameters as shown.

ParameterConsole groupDefaultAllowed valuesWhat it does
TenantIdConnection to Rhino Relay (filled in for you)nonepattern ^[0-9A-HJKMNP-TV-Z]{26}$Your Rhino Relay tenant ID. Filled in for you.
ExternalIdConnection to Rhino Relay (filled in for you)nonepattern ^[0-9a-f]{64}$; at most 64 characters; hidden in the console (NoEcho)External ID that Rhino Relay must present to assume the access role. Filled in for you.
SendingDomainConnection to Rhino Relay (filled in for you)nonepattern ^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]([a-z0-9-]{0,61}[a-z0-9])$; at most 247 charactersDomain your campaigns are sent from. Filled in for you.
ArchiveRetentionDaysData400at least 1Days to keep raw SES events in the archive bucket.
KmsKeyArnDataemptypattern ^(arn:aws[a-z-]*:kms:[a-z0-9-]+:[0-9]{12}:key/[a-zA-Z0-9-]+)?$Optional: ARN of your own KMS key (in this region) for the tables, buckets and the rules' dead-letter queue. Leave empty to use AWS-owned keys. If you set it, add the statements from the KmsKeyPolicyStatements output to the key policy.
CreateTrailAudit loggingfalsetrue or falseCreate a CloudTrail trail with its own Object Lock bucket. Leave false if an organization trail already records this account (a second trail is charged).
DataEventsAudit loggingfalsetrue or falseAlso record data events (table and bucket reads and writes) in the trail. Takes effect only when CreateTrail is true. Charged per event.
DisconnectAccessfalsetrue or falseSet to true and update the stack to switch Rhino Relay off: its access and the events sent to it stop. Your data stays.
Logical IDTypeWhat it's forWhen
ContactsAWS::DynamoDB::TableContacts, custom fields, tags, consent records and engagement scores. Kept if you delete the stack.Always
AppAWS::DynamoDB::TableTemplates, segments, campaigns, send records, counters, imports, settings and idempotency records. Kept if you delete the stack.Always
ImportsAWS::S3::Bucketrhino-relay-imports-<your account>-<region>. CSV uploads, through presigned URLs our API issues. Sends S3 events to EventBridge. Objects expire after 7 days. Kept if you delete the stack.Always
ExportsAWS::S3::Bucketrhino-relay-exports-<your account>-<region>. Exports you ask for, downloaded through presigned URLs. Objects expire after 7 days. Kept if you delete the stack.Always
ArchiveAWS::S3::Bucketrhino-relay-archive-<your account>-<region>. The full SES event archive, written by Firehose. Objects expire after ArchiveRetentionDays. Kept if you delete the stack.Always
ImportsPolicyAWS::S3::BucketPolicyRefuses any request to the Imports bucket that isn't over TLS.Always
ExportsPolicyAWS::S3::BucketPolicyRefuses any request to the Exports bucket that isn't over TLS.Always
ArchivePolicyAWS::S3::BucketPolicyRefuses any request to the Archive bucket that isn't over TLS.Always
RuleDlqAWS::SQS::QueueDead-letter queue for events the three EventBridge rules couldn't deliver. The only queue in your account; it holds nothing while delivery works.Always
ForwardRoleAWS::IAM::RoleLets ForwardRule and UploadsRule put events on our event bus, rhino-relay-prod-tenants, in the stack's Region.Only while Disconnect is false (removed when you set it to true)
ForwardRuleAWS::Events::RuleForwards SES events from our configuration sets to our event bus, where our rules keep only IDs, timestamps and flags.Only while Disconnect is false (removed when you set it to true)
UploadsRuleAWS::Events::RuleForwards notifications of new objects under csv/ in the Imports bucket to our event bus. The file itself stays in your bucket.Only while Disconnect is false (removed when you set it to true)
ArchiveStreamRoleAWS::IAM::RoleLets Firehose write the archive into the Archive bucket.Always
ArchiveStreamAWS::KinesisFirehose::DeliveryStreamFirehose stream: buffers 300 seconds or 5 MB, compresses with GZIP, writes to Archive.Always
ArchiveRuleRoleAWS::IAM::RoleLets ArchiveRule put records on ArchiveStream.Always
ArchiveRuleAWS::Events::RuleSends the full SES events from our configuration sets to the ArchiveStream Firehose stream.Always
RuleDlqPolicyAWS::SQS::QueuePolicyLets this stack's EventBridge rules send failed deliveries to RuleDlq. Refuses requests without TLS.Always
RuleDlqAlarmAWS::CloudWatch::AlarmFires when RuleDlq has a message. No actions.Always
ArchiveDatabaseAWS::Glue::DatabaseGlue database for querying the archive with Athena.Always
ArchiveTableAWS::Glue::TableGlue table ses_events over the archive, with partition projection by day.Always
TrailBucketAWS::S3::Bucketrhino-relay-trail-<your account>-<region>. CloudTrail log bucket with 400-day Object Lock. Kept if you delete the stack.Only when CreateTrail is true
TrailBucketPolicyAWS::S3::BucketPolicyLets CloudTrail write this account's logs to TrailBucket. Refuses requests without TLS.Only when CreateTrail is true
TrailAWS::CloudTrail::TrailMulti-Region trail: management events, and data events if you turn them on.Only when CreateTrail is true
AccessRoleAWS::IAM::RoleThe one role our service uses in your account, rhino-relay-access-<TenantId>.Always
AccessPolicyAWS::IAM::PolicyThe access role's permissions: full control of what this stack creates for us, and all of SES.Only while Disconnect is false (removed when you set it to true)
StackParameterAWS::SSM::ParameterSSM parameter /rhino-relay/stack with the stack ID, template version and outputs, so we can find the stack.Always

Tables and buckets have DeletionPolicy and UpdateReplacePolicy set to Retain: deleting the stack, or a template update, never deletes your data. RuleDlq is deleted with the stack.

Logical ID Keys Settings
Contacts Partition key contactId (string) On-demand, point-in-time recovery, deletion protection. Encrypted with your key if KmsKeyArn is set, otherwise the AWS-owned key.
App Partition key PK, sort key SK (strings); index batch (GSI_PK, GSI_SK); index list (LIST_PK, LIST_SK); TTL attribute ttl Same as Contacts.

Bucket names are readable, so you can tell them apart in your account: rhino-relay-<purpose>-<your account>-<region>.

Logical ID Name Objects expire after Contents
Imports rhino-relay-imports-<your account>-<region> 7 days CSV uploads. The bucket sends S3 events to EventBridge, and UploadsRule forwards new objects under csv/ to us. A CORS rule lets the console upload a file from your browser (PUT from https://console.rhinorelay.com only; the presigned URL is what authorizes it).
Exports rhino-relay-exports-<your account>-<region> 7 days Exports you ask for.
Archive rhino-relay-archive-<your account>-<region> ArchiveRetentionDays (default 400) The full SES event archive, written by Firehose under ses/yyyy/MM/dd/, with errors under errors/.

All three block public access (all four settings), use BucketOwnerEnforced object ownership (no ACLs), encrypt with SSE-S3 or with your key and an S3 bucket key, and abort incomplete multipart uploads after a day. Server access logging is off. With DataEvents on, CloudTrail records object-level access instead. Each bucket policy refuses requests that aren’t over TLS:

Bucket policy of <Imports>:

{
"Sid": "DenyInsecureTransport",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"<Imports.Arn>",
"<Imports.Arn>/*"
],
"Condition": {
"Bool": {
"aws:SecureTransport": "false"
}
}
}

Bucket policy of <Exports>:

{
"Sid": "DenyInsecureTransport",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"<Exports.Arn>",
"<Exports.Arn>/*"
],
"Condition": {
"Bool": {
"aws:SecureTransport": "false"
}
}
}

Bucket policy of <Archive>:

{
"Sid": "DenyInsecureTransport",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"<Archive.Arn>",
"<Archive.Arn>/*"
],
"Condition": {
"Bool": {
"aws:SecureTransport": "false"
}
}
}

All three rules are on your default event bus. The two SES rules match SES events from our configuration sets only: source aws.ses, the nine sending event types, and the ses:configuration-set tag starting with rhino-relay-. Events from your own configuration sets aren’t matched.

  • ForwardRule forwards each SES event to our event bus, using ForwardRole. EventBridge can’t transform an event on its way to another account’s bus, so the event leaves your account whole. Our rules on our bus then keep only the fields below before anything reaches our events queue. Those rules have no dead-letter queue, so a full event is never stored in our account. Recipient addresses, the sender, subjects and headers never reach our queue. The IP address of an open doesn’t either: our rules only note whether the open came from Apple’s machine-open proxy. A click names the link by its position in your template, never its URL. Failed deliveries go to RuleDlq.
  • UploadsRule forwards notifications of new objects under csv/ in your Imports bucket to our bus, using ForwardRole. Our rule keeps the account, Region, bucket name and object key (csv/<import ID>.csv). The file itself never leaves your bucket: our importer reads it from there. Failed deliveries go to RuleDlq.
  • ArchiveRule sends the full SES event to the ArchiveStream Firehose stream in your account, using ArchiveRuleRole. Failed deliveries go to RuleDlq. It stays when you disconnect, so your archive is complete.

The fields our events queue gets from each SES event:

Field in the messageFrom the SES event
account$.account
region$.region
eventId$.id
eventType$.detail.eventType
messageId$.detail.mail.messageId
mailTimestamp$.detail.mail.timestamp
tags$.detail.mail.tags
bounceType$.detail.bounce.bounceType
bounceSubType$.detail.bounce.bounceSubType
bounceTimestamp$.detail.bounce.timestamp
complaintTimestamp$.detail.complaint.timestamp
complaintSubType$.detail.complaint.complaintSubType
deliveryTimestamp$.detail.delivery.timestamp
delayTimestamp$.detail.deliveryDelay.timestamp
openTimestamp$.detail.open.timestamp
openBot$.detail.open.isBotEvent
clickTimestamp$.detail.click.timestamp
clickBot$.detail.click.isBotEvent
linkTags$.detail.click.linkTags

account and region are set by EventBridge from where the event happened, so one client’s events can never be taken for another’s.

Trust policy of ForwardRole, only while Disconnect is false (removed when you set it to true):

{
"Effect": "Allow",
"Principal": {
"Service": "events.amazonaws.com"
},
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {
"aws:SourceAccount": "<your account>"
}
}
}

Inline policy forward-to-rhino-relay of ForwardRole, only while Disconnect is false (removed when you set it to true):

{
"Effect": "Allow",
"Action": "events:PutEvents",
"Resource": "arn:aws:events:<region>:<our account>:event-bus/rhino-relay-prod-tenants"
}

RuleDlq keeps events a rule couldn’t deliver for 14 days, encrypted with SSE-SQS or your key. RuleDlqAlarm fires when it holds a message, with no actions: it shows in your CloudWatch console.

Queue policy of <RuleDlq>:

{
"Sid": "DenyInsecureTransport",
"Effect": "Deny",
"Principal": "*",
"Action": "sqs:*",
"Resource": [
"<RuleDlq.Arn>"
],
"Condition": {
"Bool": {
"aws:SecureTransport": "false"
}
}
}
{
"Sid": "EventBridgeFailedDeliveries",
"Effect": "Allow",
"Principal": {
"Service": "events.amazonaws.com"
},
"Action": "sqs:SendMessage",
"Resource": "<RuleDlq.Arn>",
"Condition": {
"ArnLike": {
"aws:SourceArn": "arn:aws:events:<region>:<your account>:rule/<this stack's name>-*"
},
"StringEquals": {
"aws:SourceAccount": "<your account>"
}
}
}

ArchiveStream is a direct-put stream encrypted with an AWS-owned key. It buffers for 300 seconds or 5 MB, compresses with GZIP, adds a newline after each event, and writes to the Archive bucket (with your key if KmsKeyArn is set).

Trust policy of ArchiveStreamRole:

{
"Effect": "Allow",
"Principal": {
"Service": "firehose.amazonaws.com"
},
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {
"aws:SourceAccount": "<your account>"
}
}
}

Inline policy archive-write of ArchiveStreamRole:

{
"Sid": "ArchiveBucket",
"Effect": "Allow",
"Action": [
"s3:GetBucketLocation",
"s3:ListBucket",
"s3:ListBucketMultipartUploads"
],
"Resource": "<Archive.Arn>"
}
{
"Sid": "ArchiveObjects",
"Effect": "Allow",
"Action": [
"s3:AbortMultipartUpload",
"s3:GetObject",
"s3:PutObject"
],
"Resource": "<Archive.Arn>/*"
}

ClientKey: only when KmsKeyArn is set.

{
"Sid": "ClientKey",
"Effect": "Allow",
"Action": [
"kms:GenerateDataKey",
"kms:Decrypt"
],
"Resource": "<KmsKeyArn>",
"Condition": {
"StringEquals": {
"kms:ViaService": "s3.<region>.amazonaws.com"
}
}
}

Trust policy of ArchiveRuleRole:

{
"Effect": "Allow",
"Principal": {
"Service": "events.amazonaws.com"
},
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {
"aws:SourceAccount": "<your account>"
}
}
}

Inline policy archive-put of ArchiveRuleRole:

{
"Effect": "Allow",
"Action": [
"firehose:PutRecord",
"firehose:PutRecordBatch"
],
"Resource": "<ArchiveStream.Arn>"
}

ArchiveDatabase is named rhino_relay_ plus the first 8 characters of the stack ID’s UUID. ArchiveTable, ses_events, reads s3://<archive bucket>/ses/ with the OpenX JSON SerDe and partition projection on dt (yyyy/MM/dd), so no crawler or partition loading is needed. See Querying the archive with Athena.

Only with CreateTrail set to true. Leave it off if an organization trail already records the account: a second trail is charged.

  • Trail: multi-Region, management events, global service events and log file validation. With DataEvents on, it also records data events for the two tables and the three buckets’ objects.
  • TrailBucket (rhino-relay-trail-<your account>-<region>): S3 Object Lock in governance mode with 400-day default retention, versioning, public access blocked, BucketOwnerEnforced, SSE-S3, logs expire after 400 days (old versions a day later), kept if you delete the stack. A user with s3:BypassGovernanceRetention can still remove locked logs.

The access role has no permissions on the trail or its bucket.

Bucket policy of <TrailBucket>, only when CreateTrail is true:

{
"Sid": "DenyInsecureTransport",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"<TrailBucket.Arn>",
"<TrailBucket.Arn>/*"
],
"Condition": {
"Bool": {
"aws:SecureTransport": "false"
}
}
}
{
"Sid": "CloudTrailAclCheck",
"Effect": "Allow",
"Principal": {
"Service": "cloudtrail.amazonaws.com"
},
"Action": "s3:GetBucketAcl",
"Resource": "<TrailBucket.Arn>",
"Condition": {
"StringEquals": {
"aws:SourceAccount": "<your account>"
},
"ArnLike": {
"aws:SourceArn": "arn:aws:cloudtrail:<region>:<your account>:trail/*"
}
}
}
{
"Sid": "CloudTrailWrite",
"Effect": "Allow",
"Principal": {
"Service": "cloudtrail.amazonaws.com"
},
"Action": "s3:PutObject",
"Resource": "<TrailBucket.Arn>/AWSLogs/<your account>/*",
"Condition": {
"StringEquals": {
"s3:x-amz-acl": "bucket-owner-full-control",
"aws:SourceAccount": "<your account>"
},
"ArnLike": {
"aws:SourceArn": "arn:aws:cloudtrail:<region>:<your account>:trail/*"
}
}
}

AccessRole is named rhino-relay-access-<TenantId>. It’s the only IAM role name the template sets, so we know its ARN before the stack exists. Its maximum session is one hour.

While you’re connected, only our service role in the stack’s Region can assume the role, and only with your external ID. All our functions share that one role. Each session carries a SourceIdentity naming the function that opened it, such as sender or api, so your CloudTrail shows which part of our service acted. With Disconnect set to true the trust policy trusts only your own account, because a trust policy must have a statement.

Trust policy of AccessRole, only while Disconnect is false (removed when you set it to true):

{
"Sid": "RhinoRelayService",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::<our account>:root"
},
"Action": [
"sts:AssumeRole",
"sts:SetSourceIdentity"
],
"Condition": {
"StringEquals": {
"sts:ExternalId": "<ExternalId>",
"aws:PrincipalArn": "arn:aws:iam::<our account>:role/rhino-relay-prod-service-<region>"
}
}
}

Trust policy of AccessRole, only when Disconnect is true:

{
"Sid": "OwnAccountOnly",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::<your account>:root"
},
"Action": "sts:AssumeRole"
}

AccessPolicy is the policy rhino-relay-access. It gives our service full control of what this stack creates for us, and all of SES in your account:

  • the two tables and their indexes, and the three buckets and their objects,
  • all of SES, which we use for your sending domain, its tracking subdomain, our two configuration sets (rhino-relay-campaigns and rhino-relay-test) and your account’s sending status,
  • SSM parameters under /rhino-relay/, which hold Mailchimp credentials during an import and the stack parameter,
  • the three EventBridge rules, RuleDlq and its alarm, ArchiveStream and the Glue database,
  • putting our events on your default bus, reading CloudWatch metrics, and reading this stack,
  • your KMS key, if you set KmsKeyArn.

It grants nothing else. The role can’t change the template’s IAM roles (including itself), your CloudTrail trail or its bucket, or the stack. It’s deleted when you set Disconnect to true, so sessions opened earlier lose their permissions too.

Policy rhino-relay-access of <AccessRole>, only while Disconnect is false (removed when you set it to true):

{
"Sid": "Tables",
"Effect": "Allow",
"Action": [
"dynamodb:*"
],
"Resource": [
"<Contacts.Arn>",
"<App.Arn>",
"<Contacts.Arn>/*",
"<App.Arn>/*"
]
}
{
"Sid": "Buckets",
"Effect": "Allow",
"Action": [
"s3:*"
],
"Resource": [
"<Imports.Arn>",
"<Exports.Arn>",
"<Archive.Arn>",
"<Imports.Arn>/*",
"<Exports.Arn>/*",
"<Archive.Arn>/*"
]
}
{
"Sid": "Ses",
"Effect": "Allow",
"Action": [
"ses:*"
],
"Resource": [
"*"
]
}
{
"Sid": "Parameters",
"Effect": "Allow",
"Action": [
"ssm:*"
],
"Resource": [
"arn:aws:ssm:<region>:<your account>:parameter/rhino-relay/*"
]
}
{
"Sid": "ForwardingRules",
"Effect": "Allow",
"Action": [
"events:*"
],
"Resource": [
"<ForwardRule.Arn>",
"<UploadsRule.Arn>",
"<ArchiveRule.Arn>"
]
}
{
"Sid": "ClientEvents",
"Effect": "Allow",
"Action": [
"events:PutEvents"
],
"Resource": [
"arn:aws:events:<region>:<your account>:event-bus/default"
]
}
{
"Sid": "RuleDeadLetterQueue",
"Effect": "Allow",
"Action": [
"sqs:*"
],
"Resource": [
"<RuleDlq.Arn>"
]
}
{
"Sid": "ArchiveStream",
"Effect": "Allow",
"Action": [
"firehose:*"
],
"Resource": [
"<ArchiveStream.Arn>"
]
}
{
"Sid": "ArchiveCatalog",
"Effect": "Allow",
"Action": [
"glue:*"
],
"Resource": [
"arn:aws:glue:<region>:<your account>:catalog",
"arn:aws:glue:<region>:<your account>:database/<ArchiveDatabase>",
"arn:aws:glue:<region>:<your account>:table/<ArchiveDatabase>/*"
]
}
{
"Sid": "Alarms",
"Effect": "Allow",
"Action": [
"cloudwatch:*"
],
"Resource": [
"<RuleDlqAlarm.Arn>"
]
}
{
"Sid": "OwnStack",
"Effect": "Allow",
"Action": [
"cloudformation:DescribeStacks",
"cloudformation:DescribeStackResources",
"cloudformation:ListStackResources",
"cloudformation:GetTemplate"
],
"Resource": [
"<this stack's ID>"
]
}
{
"Sid": "Metrics",
"Effect": "Allow",
"Action": [
"cloudwatch:GetMetricData",
"cloudwatch:ListMetrics"
],
"Resource": [
"*"
]
}

ClientKey: only when KmsKeyArn is set.

{
"Sid": "ClientKey",
"Effect": "Allow",
"Action": [
"kms:Decrypt",
"kms:Encrypt",
"kms:GenerateDataKey",
"kms:DescribeKey"
],
"Resource": "<KmsKeyArn>"
}

StackParameter is the SSM String parameter /rhino-relay/stack, the template’s other fixed name. It holds the stack ID, the template version and every output below as JSON. Quick Launch gives every buyer’s stack the same name, so our Verify step finds your stack through this parameter instead of by name.

OutputValue
TemplateVersionThe template version you installed.
AccessRoleArnARN of the access role, rhino-relay-access-<TenantId>.
ContactsTableNameName of the contacts table.
AppTableNameName of the app table.
ImportsBucketNameName of the imports bucket, rhino-relay-imports-<your account>-<region>.
ExportsBucketNameName of the exports bucket, rhino-relay-exports-<your account>-<region>.
ArchiveBucketNameName of the archive bucket, rhino-relay-archive-<your account>-<region>.
GlueDatabaseNameName of the Glue database that holds the ses_events table.
DisconnectedThe Disconnect setting: true when our access is removed.
StackIdThis stack's ID.
KmsKeyPolicyStatementsThe statement to add to your KMS key policy. Present only when KmsKeyArn is set.

CloudFormation can’t change the policy of a key that already exists. When you set KmsKeyArn, add this statement to your key policy yourself, so EventBridge can write failed deliveries to the encrypted RuleDlq. The stack prints it, with your account filled in, as the KmsKeyPolicyStatements output:

[
{
"Sid": "EventBridgeToEncryptedQueue",
"Effect": "Allow",
"Principal": {
"Service": "events.amazonaws.com"
},
"Action": [
"kms:GenerateDataKey",
"kms:Decrypt"
],
"Resource": "*",
"Condition": {
"StringEquals": {
"aws:SourceAccount": "<your account>"
}
}
}
]
  • "Resource": "*" in a key policy means this key.
  • The access role, ArchiveStreamRole and whoever deploys the stack are authorized by IAM policies in your account, so keep the key policy’s default statement that lets IAM policies grant use of the key. The key policy grants nothing to our account.
  • The key must be a symmetric key in the stack’s Region.

List your stack’s resources and compare them with the table above.

Terminal window
aws cloudformation list-stack-resources --stack-name rhino-relay \
--query 'StackResourceSummaries[].[LogicalResourceId,ResourceType]' --output table

Check it worked: every logical ID in the list is on this page, and CreateTrail resources appear only if you turned the trail on.