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:
- 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. - The forwarding rules,
ForwardRuleandUploadsRule, 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.ForwardRolelets 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.
Parameters
Section titled “Parameters”Marketplace fills in the first three for you. The console groups the parameters as shown.
| Parameter | Console group | Default | Allowed values | What it does |
|---|---|---|---|---|
TenantId | Connection to Rhino Relay (filled in for you) | none | pattern ^[0-9A-HJKMNP-TV-Z]{26}$ | Your Rhino Relay tenant ID. Filled in for you. |
ExternalId | Connection to Rhino Relay (filled in for you) | none | pattern ^[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. |
SendingDomain | Connection to Rhino Relay (filled in for you) | none | pattern ^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]([a-z0-9-]{0,61}[a-z0-9])$; at most 247 characters | Domain your campaigns are sent from. Filled in for you. |
ArchiveRetentionDays | Data | 400 | at least 1 | Days to keep raw SES events in the archive bucket. |
KmsKeyArn | Data | empty | pattern ^(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. |
CreateTrail | Audit logging | false | true or false | Create a CloudTrail trail with its own Object Lock bucket. Leave false if an organization trail already records this account (a second trail is charged). |
DataEvents | Audit logging | false | true or false | Also record data events (table and bucket reads and writes) in the trail. Takes effect only when CreateTrail is true. Charged per event. |
Disconnect | Access | false | true or false | Set to true and update the stack to switch Rhino Relay off: its access and the events sent to it stop. Your data stays. |
Every resource
Section titled “Every resource”| Logical ID | Type | What it's for | When |
|---|---|---|---|
Contacts | AWS::DynamoDB::Table | Contacts, custom fields, tags, consent records and engagement scores. Kept if you delete the stack. | Always |
App | AWS::DynamoDB::Table | Templates, segments, campaigns, send records, counters, imports, settings and idempotency records. Kept if you delete the stack. | Always |
Imports | AWS::S3::Bucket | rhino-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 |
Exports | AWS::S3::Bucket | rhino-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 |
Archive | AWS::S3::Bucket | rhino-relay-archive-<your account>-<region>. The full SES event archive, written by Firehose. Objects expire after ArchiveRetentionDays. Kept if you delete the stack. | Always |
ImportsPolicy | AWS::S3::BucketPolicy | Refuses any request to the Imports bucket that isn't over TLS. | Always |
ExportsPolicy | AWS::S3::BucketPolicy | Refuses any request to the Exports bucket that isn't over TLS. | Always |
ArchivePolicy | AWS::S3::BucketPolicy | Refuses any request to the Archive bucket that isn't over TLS. | Always |
RuleDlq | AWS::SQS::Queue | Dead-letter queue for events the three EventBridge rules couldn't deliver. The only queue in your account; it holds nothing while delivery works. | Always |
ForwardRole | AWS::IAM::Role | Lets 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) |
ForwardRule | AWS::Events::Rule | Forwards 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) |
UploadsRule | AWS::Events::Rule | Forwards 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) |
ArchiveStreamRole | AWS::IAM::Role | Lets Firehose write the archive into the Archive bucket. | Always |
ArchiveStream | AWS::KinesisFirehose::DeliveryStream | Firehose stream: buffers 300 seconds or 5 MB, compresses with GZIP, writes to Archive. | Always |
ArchiveRuleRole | AWS::IAM::Role | Lets ArchiveRule put records on ArchiveStream. | Always |
ArchiveRule | AWS::Events::Rule | Sends the full SES events from our configuration sets to the ArchiveStream Firehose stream. | Always |
RuleDlqPolicy | AWS::SQS::QueuePolicy | Lets this stack's EventBridge rules send failed deliveries to RuleDlq. Refuses requests without TLS. | Always |
RuleDlqAlarm | AWS::CloudWatch::Alarm | Fires when RuleDlq has a message. No actions. | Always |
ArchiveDatabase | AWS::Glue::Database | Glue database for querying the archive with Athena. | Always |
ArchiveTable | AWS::Glue::Table | Glue table ses_events over the archive, with partition projection by day. | Always |
TrailBucket | AWS::S3::Bucket | rhino-relay-trail-<your account>-<region>. CloudTrail log bucket with 400-day Object Lock. Kept if you delete the stack. | Only when CreateTrail is true |
TrailBucketPolicy | AWS::S3::BucketPolicy | Lets CloudTrail write this account's logs to TrailBucket. Refuses requests without TLS. | Only when CreateTrail is true |
Trail | AWS::CloudTrail::Trail | Multi-Region trail: management events, and data events if you turn them on. | Only when CreateTrail is true |
AccessRole | AWS::IAM::Role | The one role our service uses in your account, rhino-relay-access-<TenantId>. | Always |
AccessPolicy | AWS::IAM::Policy | The 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) |
StackParameter | AWS::SSM::Parameter | SSM 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.
DynamoDB tables
Section titled “DynamoDB tables”| 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. |
S3 buckets
Section titled “S3 buckets”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" } }}EventBridge rules
Section titled “EventBridge rules”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.
ForwardRuleforwards each SES event to our event bus, usingForwardRole. 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 toRuleDlq.UploadsRuleforwards notifications of new objects undercsv/in yourImportsbucket to our bus, usingForwardRole. 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 toRuleDlq.ArchiveRulesends the full SES event to theArchiveStreamFirehose stream in your account, usingArchiveRuleRole. Failed deliveries go toRuleDlq. It stays when you disconnect, so your archive is complete.
The fields our events queue gets from each SES event:
| Field in the message | From 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>" } }}Data Firehose and its roles
Section titled “Data Firehose and its roles”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>"}Glue database and table
Section titled “Glue database and table”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.
CloudTrail trail (optional)
Section titled “CloudTrail trail (optional)”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. WithDataEventson, 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 withs3:BypassGovernanceRetentioncan 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/*" } }}The access role
Section titled “The access role”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.
Trust policy
Section titled “Trust policy”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"}Permission policy
Section titled “Permission policy”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-campaignsandrhino-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,
RuleDlqand its alarm,ArchiveStreamand 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>"}Stack parameter
Section titled “Stack parameter”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.
Outputs
Section titled “Outputs”| Output | Value |
|---|---|
TemplateVersion | The template version you installed. |
AccessRoleArn | ARN of the access role, rhino-relay-access-<TenantId>. |
ContactsTableName | Name of the contacts table. |
AppTableName | Name of the app table. |
ImportsBucketName | Name of the imports bucket, rhino-relay-imports-<your account>-<region>. |
ExportsBucketName | Name of the exports bucket, rhino-relay-exports-<your account>-<region>. |
ArchiveBucketName | Name of the archive bucket, rhino-relay-archive-<your account>-<region>. |
GlueDatabaseName | Name of the Glue database that holds the ses_events table. |
Disconnected | The Disconnect setting: true when our access is removed. |
StackId | This stack's ID. |
KmsKeyPolicyStatements | The statement to add to your KMS key policy. Present only when KmsKeyArn is set. |
Your own KMS key
Section titled “Your own KMS key”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,
ArchiveStreamRoleand 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.
Check what you installed
Section titled “Check what you installed”List your stack’s resources and compare them with the table above.
aws cloudformation list-stack-resources --stack-name rhino-relay \ --query 'StackResourceSummaries[].[LogicalResourceId,ResourceType]' --output tableCheck it worked: every logical ID in the list is on this page, and CreateTrail resources appear only if you turned the trail on.