Installation guide
This guide is for the platform team installing BucketDeck in their AWS account. The install is one CloudFormation stack (the hub). Every other account whose buckets BucketDeck should reach gets a small role stack (the spoke). With IAM Identity Center or another SAML IdP, the hub takes two steps: deploy the stack, configure the SAML application, then update the stack. For the short version, see Getting started.
Before you start
- Region. Any Region with AWS Lambda, Step Functions (Distributed Map), Amplify Hosting, Amazon Cognito, AWS License Manager and ECS Fargate. The templates are always loaded from the vendor's public bucket in us-east-1 (CloudFormation accepts a template URL from any Region), and all product code comes from the container image. The hub's Region doesn't limit which Regions' buckets you can browse.
- Account. Deploy the hub in the account that subscribed on AWS Marketplace: the licence is delivered to that account through AWS License Manager. To run the hub in another account of your organization, distribute the licence to it with a License Manager grant first.
- Permissions. You need permission to create the resources in the stack: IAM roles (three of them have fixed names), Lambda, Step Functions, ECS, EC2 security groups, Cognito, API Gateway, Amplify, DynamoDB, S3 and CloudWatch Logs.
- Network. A VPC with at least one subnet with outbound HTTPS access, for the one-off installer task (see Networking). Nothing else runs in your VPC.
- Identity. Decide how users will sign in: IAM Identity Center, another SAML 2.0 IdP (Entra ID, Okta, ...), an OIDC IdP, or Cognito-managed users. SAML IdPs other than IAM Identity Center, and OIDC, need BucketDeck Team or above.
1. Subscribe
BucketDeck is two AWS Marketplace products: BucketDeck Starter (the Starter plan) and BucketDeck (the Team plan, with Business and Enterprise as add-ons). Both have contract pricing. For Business, choose Team and the Business add-on; for Enterprise, all three.
- Open BucketDeck or BucketDeck Starter in AWS Marketplace and choose Continue to Subscribe. Accept the offer (contract pricing). AWS License Manager now holds your licence.
- Choose Continue to Configuration, select the fulfilment option and the Region, then open the CloudFormation launch link from the usage instructions.
2. Launch the hub stack
In the CloudFormation console, fill in the parameters (see the reference). Acknowledge "AWS CloudFormation might create IAM resources with custom names" and create the stack. Creation takes about 10 minutes.
The template URL is always in the vendor's public bucket in us-east-1, whichever Region you deploy to:
https://cloudcertainty-bucketdeck-us-east-1.s3.us-east-1.amazonaws.com/<edition>/<version>/main.yaml<edition> is starter for BucketDeck Starter and pro for BucketDeck.
From the CLI (--region is the Region you deploy to):
aws cloudformation create-stack --region <your-region> --stack-name bucketdeck \
--template-url https://cloudcertainty-bucketdeck-us-east-1.s3.us-east-1.amazonaws.com/<edition>/<version>/main.yaml \
--parameters file://params.json \
--capabilities CAPABILITY_IAM CAPABILITY_NAMED_IAMparams.json lists the parameters as [{"ParameterKey": "VpcId", "ParameterValue": "vpc-..."}, ...].
What happens during creation
The product's code (Lambda functions and the web portal) ships inside the subscription-protected container image, not as public downloads. Early in stack creation:
- CloudFormation creates a private S3 bucket in your account (output
ArtifactsBucketName; all public access blocked, encrypted, TLS only). - The
ArtifactInstallcustom resource starts one Fargate task from the product image in yourInstallerSubnetIds. The task checks your AWS Marketplace licence in AWS License Manager, verifies the code bundle against the checksums in the image, and copies it into the bucket. It runs for about a minute at 0.25 vCPU / 512 MB, with a read-only root filesystem and a security group without inbound rules. - The Lambda functions, the job state machine and the portal are then created from that bucket.
If the installer task fails, stack creation fails and rolls back. The error message gives the task's
exit code and its log stream (log group /<NamePrefix>/installer); see
Troubleshooting.
3. Connect your identity provider
IdentityProvider | What to do |
|---|---|
IAMIdentityCenter | Follow Identity providers. Create the app with the SamlAcsUrl and SamlAudienceUri outputs, then update the stack with SamlMetadataUrl. |
SAML | Team and above. Do the same in your IdP (Entra ID and Okta are covered in Identity providers). |
OIDC | Team and above. Put the client secret in Secrets Manager, then set OidcIssuer, OidcClientId and OidcClientSecretArn when you create the stack. Register <CognitoDomain>/oauth2/idpresponse as the redirect URI in your IdP. |
CognitoOnly | Nothing more to do. BootstrapAdminEmail receives a temporary password. Every user must register an authenticator app (TOTP MFA is required). |
Until SamlMetadataUrl is set, the portal falls back to Cognito sign-in, which has no users. Nobody
can sign in at that point, so the stack is safe in that state.
4. First sign-in
Open the PortalUrl output and sign in as BootstrapAdminEmail. The first account that signs in
with that (verified) email becomes the bootstrap administrator and stays bound to it (see
admin guide).
Administrators see every connection and bucket. Other users see nothing until an administrator grants them access (admin guide).
5. Connections
A connection is one way into one AWS account. The local connection to the hub's own account
exists from the start: it uses the stack's LocalAccessRole, limited to the buckets in
LocalBucketAccess, with the actions of LocalAccessLevel. BucketDeck's own data and artifacts
buckets are always denied.
To reach another account, add a connection in Administration → Connections:
| Type | Use when | Credentials |
|---|---|---|
| Role (recommended) | The account can host an IAM role | BucketDeck assumes the spoke role with an external ID; credentials last one hour and are scoped per request |
| Access keys | Only an IAM user is possible (for example a partner account you don't administer) | Stored in Secrets Manager under <NamePrefix>/connections/; write-only in the API. Requires AccessKeyConnections=Enabled |
For a role connection, the console shows the external ID, the trusted principals and a quick-create
link for the spoke template. Deploy the spoke role in the target account (next section), enter its
RoleArn output in the connection and choose Test. Custom roles you create by hand must use the
IAM path /bucketdeck/: the hub roles may assume only roles under that path.
6. Deploy the spoke role in other accounts
spoke-role.yaml creates one role, /bucketdeck/<RoleName>, that only the hub roles (stack output
SpokeTrustedPrincipals) can assume, and only with your external ID. Its template URL is the stack
output SpokeTemplateUrl:
https://cloudcertainty-bucketdeck-us-east-1.s3.us-east-1.amazonaws.com/<edition>/<version>/spoke-role.yaml| Parameter | Default | Meaning |
|---|---|---|
TrustedPrincipals | The hub's SpokeTrustedPrincipals output (two role ARNs, comma separated) | |
ExternalId | 16–128 characters; the connection's external ID from the admin console | |
RoleName | BucketDeckAccess | Role name (path /bucketdeck/) |
AccessLevel | ReadWrite | ReadOnly (list, read, preview, download), ReadWrite (plus upload and copy in), Full (plus delete, move, restore, bucket CORS for uploads) |
BucketScope | * | * or a comma-separated list of bucket names (wildcards such as logs-* allowed) |
AllowCloudWatchMetrics | true | Read S3 storage metrics for Bucket Lens (Business and above) |
The role is the upper limit of what BucketDeck can do in that account; grants narrow it per user. IAM is global, so deploy the stack once per account, in any one Region.
A single account
Use the quick-create link from Administration → Connections → Set up role, or:
cat > spoke-params.json <<'EOF'
[
{"ParameterKey": "TrustedPrincipals", "ParameterValue": "<SpokeTrustedPrincipals output>"},
{"ParameterKey": "ExternalId", "ParameterValue": "<external id>"},
{"ParameterKey": "AccessLevel", "ParameterValue": "ReadWrite"}
]
EOF
aws cloudformation create-stack --region us-east-1 --stack-name bucketdeck-spoke \
--template-url https://cloudcertainty-bucketdeck-us-east-1.s3.us-east-1.amazonaws.com/<edition>/<version>/spoke-role.yaml \
--parameters file://spoke-params.json --capabilities CAPABILITY_NAMED_IAMEvery account of an AWS Organization (StackSets)
With service-managed StackSets, new accounts in the target OUs get the role automatically. Use one external ID for the whole StackSet, and enter that same external ID for every connection you create for those accounts (the connection form lets you set it).
# From the management account or a delegated StackSets administrator.
aws cloudformation create-stack-set --region us-east-1 --stack-set-name bucketdeck-spoke \
--template-url https://cloudcertainty-bucketdeck-us-east-1.s3.us-east-1.amazonaws.com/<edition>/<version>/spoke-role.yaml \
--parameters file://spoke-params.json \
--capabilities CAPABILITY_NAMED_IAM \
--permission-model SERVICE_MANAGED \
--auto-deployment Enabled=true,RetainStacksOnAccountRemoval=false
# One Region only: the role is global.
aws cloudformation create-stack-instances --region us-east-1 --stack-set-name bucketdeck-spoke \
--deployment-targets OrganizationalUnitIds=ou-abcd-11111111 --regions us-east-1Service-managed StackSets don't deploy to the management account; use a normal stack there if
BucketDeck should reach it. To change AccessLevel or BucketScope later, update the StackSet.
7. Enable browser uploads
Uploads go straight from the browser to S3 with presigned URLs, so the target bucket needs a CORS rule for the portal origin. Previews and downloads need none.
In the portal: a user with the
managepermission on the bucket opens the bucket's details and chooses Enable browser uploads. BucketDeck adds (or updates) the CORS rulebucketdeck-uploadsand keeps any other rules. The connection's role needss3:PutBucketCORS(spokeAccessLevel=Full, or the local role).Yourself (for example with infrastructure as code), add this rule to the bucket's CORS configuration, with your
PortalUrlas the origin:json{ "ID": "bucketdeck-uploads", "AllowedOrigins": ["https://main.<app id>.amplifyapp.com"], "AllowedMethods": ["PUT"], "AllowedHeaders": ["*"], "ExposeHeaders": ["ETag"], "MaxAgeSeconds": 3000 }
Parameter reference
| Parameter | Default | Description |
|---|---|---|
NamePrefix | bucketdeck | 2–20 lowercase characters. It prefixes resource names and role names, so it must be unique in the account and Region. Don't use aws, amazon or cognito, because it forms part of the Cognito domain. |
TagOwner, TagCostCenter, TagEnvironment | Applied as the Owner, CostCenter and Environment tags on every taggable resource. | |
IdentityProvider | IAMIdentityCenter | IAMIdentityCenter, SAML, OIDC or CognitoOnly. The BucketDeck Starter template offers IAMIdentityCenter and CognitoOnly; SAML and OIDC need BucketDeck Team or above. |
SamlMetadataUrl | empty | The metadata URL from your SAML app. Leave it empty on the first deployment. |
OidcIssuer, OidcClientId, OidcClientSecretArn | empty | Required when IdentityProvider=OIDC. OidcClientSecretArn is a Secrets Manager secret ARN; CloudFormation resolves it at deploy time. |
OidcEmailVerifiedClaim | email_verified | OIDC only: the IdP claim mapped to email_verified. For Entra ID use xms_edov, or prefer SAML. See Identity providers. |
BootstrapAdminEmail | The first administrator. | |
LocalBucketAccess | * | Buckets in this account the local connection may reach: * or a comma-separated list (wildcards allowed). |
LocalAccessLevel | Full | What the local connection may do there: ReadOnly, ReadWrite or Full (adds deletes, restores and bucket CORS), as the spoke role's AccessLevel. |
AccessKeyConnections | Disabled | Enabled allows connections that use IAM user access keys. |
IdentityCenterStartUrl | empty | Optional. Your AWS access portal URL (https://d-…awsapps.com/start). Lets users connect their IAM Identity Center session and browse with their own accounts and permission sets, with no connections or grants to set up. See admin guide. |
IdentityCenterRegion | empty | Region of your Identity Center instance; empty = the stack's Region. |
IamUserKeySignIn | Disabled | Enabled lets users sign in with their own IAM user access key (exchanged for temporary credentials, never stored). |
AllowedCidrs | empty | Optional initial IP allowlist. Admins manage it in the console afterwards; changing the parameter later doesn't override what admins saved. Managing the allowlist is part of Business and above. |
JobMaxConcurrency | 200 | 1–1000. Upper bound for the parallel workers of one job. Mind your Lambda concurrency quota (default 1,000 per Region). Your plan caps it too: 50 on Starter, 200 on Team, 500 on Business, 1,000 on Enterprise. |
WorkerMemoryMB | 2048 | 1024–10240. Worker memory; more memory also means more CPU and network throughput. |
AuditRetentionDays | 365 | How long audit events are kept, capped by your plan: 30 days on Starter, 90 on Team, 365 on Business, 3,650 on Enterprise. |
LogRetentionDays | 90 | Retention of all log groups. |
KmsKeyArn | empty | Empty creates a dedicated KMS key. Otherwise your own key (see Encryption key). |
VpcId, InstallerSubnetIds | Network for the one-off installer task. Subnets must be in VpcId (template rule). | |
InstallerSubnetsArePublic | false | true only for subnets that route to an internet gateway; the task then gets a public IP for outbound access. |
ArtifactVersionOverride, ImageUriOverride | empty | Advanced (support use only). Leave empty. |
Networking
Only the installer task runs in your VPC, for about a minute at install and at each upgrade. It needs outbound HTTPS (443) to:
- Amazon ECR in us-east-1, to pull the image from the AWS Marketplace registry (cross-Region, so this needs internet egress: a NAT gateway or public subnets);
- AWS License Manager in your Region, for the licence check;
- Amazon S3 in your Region, to write the code into the artifacts bucket (a gateway endpoint works);
- CloudWatch Logs in your Region, for its log stream.
Its security group has no inbound rules and one outbound rule, TCP 443. The Lambda functions don't run in your VPC.
Encryption key
The stack always encrypts with a customer managed KMS key. By default it creates one
(alias/<NamePrefix>, output EncryptionKeyArn, automatic rotation). The key encrypts the DynamoDB
table, all log groups, the data and artifacts buckets (SSE-KMS with an S3 Bucket Key) and access-key
secrets, and it wraps the data key with which BucketDeck encrypts every table item. Only
BucketDeck's API and worker roles may use it directly, so people with DynamoDB access see only
ciphertext (Security overview).
The created key is retained when the stack is deleted, like the table it protects. Its policy
gives your IAM administrators key administration (including PutKeyPolicy) but no cryptographic use.
Using your own KMS key
Set KmsKeyArn to use your own key instead. It must be a symmetric customer managed key in the same
Region, and its key policy must allow what the created key's policy allows (see EncryptionKey in
main.yaml):
- The API and worker roles, directly, with an encryption context
purposeofbucketdeck-*:json{ "Sid": "BucketDeckEnvelopeEncryption", "Effect": "Allow", "Principal": {"AWS": "arn:aws:iam::<account>:root"}, "Action": ["kms:GenerateDataKey", "kms:Decrypt"], "Resource": "*", "Condition": { "ArnEquals": {"aws:PrincipalArn": [ "arn:aws:iam::<account>:role/<NamePrefix>-api-<region>", "arn:aws:iam::<account>:role/<NamePrefix>-worker-<region>"]}, "StringLike": {"kms:EncryptionContext:purpose": "bucketdeck-*"} } } - CloudWatch Logs:json
{ "Sid": "AllowCloudWatchLogs", "Effect": "Allow", "Principal": {"Service": "logs.<region>.amazonaws.com"}, "Action": ["kms:Encrypt*", "kms:Decrypt*", "kms:ReEncrypt*", "kms:GenerateDataKey*", "kms:Describe*"], "Resource": "*", "Condition": {"ArnLike": {"kms:EncryptionContext:aws:logs:arn": "arn:aws:logs:<region>:<account>:log-group:*"}} } - Use through DynamoDB, S3 and Secrets Manager in the Region by principals of the account
(
kms:ViaService,kms:CallerAccount), pluskms:CreateGrantthrough DynamoDB withkms:GrantIsForAWSResource. The stack's roles get only the KMS actions they need. - The identity that creates or updates the stack: Lambda reads the function code from the encrypted
bucket with the deploying identity's permissions, so it needs
kms:Decrypton the key.
The key protects BucketDeck's own data. Objects in your buckets keep their own encryption; BucketDeck reads SSE-KMS objects through S3 when the role and the key policy allow it.
Stack outputs
| Output | Use |
|---|---|
PortalUrl | The URL users and admins open. Also the application start URL in your IdP. |
ApiUrl | HTTP API base URL (informational). |
CognitoDomain | Cognito managed login domain. The OIDC redirect URI is <CognitoDomain>/oauth2/idpresponse. |
UserPoolId, UserPoolClientId | Cognito identifiers. |
SamlAcsUrl, SamlAudienceUri | Values for your SAML application. |
SpokeTrustedPrincipals | Hub role ARNs for the spoke template's TrustedPrincipals. |
SpokeTemplateUrl | URL of this release's spoke-role.yaml. |
LocalAccessRoleArn | Role of the local connection. |
TableName | DynamoDB table (retained on deletion). |
DataBucketName | Data bucket for job files and index snapshots (retained on deletion). |
ArtifactsBucketName | Private bucket with the stack's code. Emptied and deleted with the stack; store nothing else in it. |
StateMachineArn | The job state machine. |
ProductVersion | The BucketDeck release the stack runs. |
Updating
Each release is a new template; there is no version parameter. To upgrade, update the stack with the new release's template (Update stack → Replace current template) and keep every other parameter value. See upgrading guide.
Uninstalling
Delete the hub stack. The DynamoDB table (
<NamePrefix>-table), the data bucket and the created KMS key are retained on purpose (audit history, job reports, index snapshots, and the key that decrypts them). The artifacts bucket and log groups are deleted with the stack. A new stack with the sameNamePrefixcan't be created while the retained table exists.To delete the table when you no longer need it: remove its resource policy (
aws dynamodb delete-resource-policy --resource-arn <table ARN>), turn off deletion protection, then delete it. Empty and delete the data bucket, and schedule the key's deletion last.Delete the spoke stacks or the StackSet in the other accounts.
Delete any
<NamePrefix>/connections/*secrets that remain in Secrets Manager.
Troubleshooting
| Symptom | Check |
|---|---|
Stack creation or update fails at ArtifactInstall | The status reason gives the installer's exit code and log stream (log group /<NamePrefix>/installer). 3: the account has no BucketDeck licence: subscribe on AWS Marketplace, or deploy in the subscribing account (see Before you start). 5: AWS License Manager stayed unavailable; retry later. 2: the image has no valid code bundle; contact support. 4: the upload failed; check SCPs, the S3 endpoint policy and, with KmsKeyArn, the key policy. No exit code, with CannotPullContainerError or ResourceInitializationError: the subnets can't reach ECR in us-east-1, S3 or CloudWatch Logs. |
| Every page says the licence is required (402) | Administration → Licence shows what License Manager returned. The subscription may have expired, or the stack runs in an account without the licence. |
A connection test fails at AssumeRole | The spoke's TrustedPrincipals must be the hub's SpokeTrustedPrincipals, the external IDs must match, and the role must be under /bucketdeck/. An SCP can also deny sts:AssumeRole or sts:SetSourceIdentity. |
access_denied on a bucket | AWS refused the call: the role's AccessLevel/BucketScope, the bucket policy, a KMS key policy or an SCP. The message names the AWS error code. |
| Uploads fail in the browser | The bucket has no CORS rule for the portal origin (Enable browser uploads). |
ip_not_allowed | Administration → Settings → IP access. The API sees the client's public IP. |
| SAML error after sign-in | ACS URL and audience match the outputs; the email attribute is sent; the user is assigned to the app. |
Something unclear or missing? Email [email protected].