← ALL DOCS
DOCS · SECURITY REVIEWERS

Security overview

This document is for security reviewers. It covers what BucketDeck protects against, how users are identified, how access is limited, where data goes and how it is encrypted. Everything described here runs in your own AWS account.

Threat model

BucketDeck gives people access to data in S3. What it must prevent:

ThreatMitigation
A user reaches data no one granted themTwo independent layers: BucketDeck grants checked by the API on every call, and IAM credentials that a per-request session policy limits to what the grants allow (see Permissions)
Someone impersonates another userFederated sign-in; the API trusts only emails the user can't choose, and binds the bootstrap admin to an immutable ID (see Identity)
A stolen token is used from elsewhereOne-hour ID tokens, revocable 8-hour refresh tokens, optional IP allowlist checked on every call
BucketDeck itself is misused to read or change its own stateThe roles users act through explicitly deny BucketDeck's data and artifacts buckets; the table is reachable only by BucketDeck's Lambdas
Actions can't be tracedApplication audit log, plus SourceIdentity = <user email> on every assumed-role session, so CloudTrail in every target account names the person
Another account's role is assumed by a third party ("confused deputy")Spoke roles trust only the hub's two role ARNs and require an external ID
Long-lived credentials leakRole connections use one-hour STS credentials. Access-key connections are off by default (AccessKeyConnections=Disabled); keys are kept in Secrets Manager and are write-only in the API
A shared link outlives the reason it was sharedManaged share links expire, can be revoked, and stop working as soon as the creator loses read; every use is audited
A colleague is tricked into approving someone else's AWS sign-in (device-code phishing)The approval page link carries no code; after approval BucketDeck checks the Identity Center user name and refuses an email that isn't the signed-in user's
Data leaves the customerNo vendor service in the data path; transfers run inside the customer's accounts (S3 server-side copy, or streamed through the worker Lambda)

Out of scope: a malicious administrator of the AWS account (they can change any IAM policy), and the security of the identity provider and of users' devices.

Controls

PrincipleControl
Strong identity on every requestEvery API route requires a Cognito ID token, validated by the API Gateway JWT authorizer (issuer and audience). The only unauthenticated route serves managed share links. Admin routes check the role on each call.
Least privilegeEach component has its own role with resource-scoped permissions (IAM summary). Users act only through roles under the IAM path /bucketdeck/, narrowed per request.
Context-aware accessIP allowlist checked on every call (Business and above); outside it, every route except the sign-in check returns 403 ip_not_allowed.
Short-lived credentialsSTS sessions of at most one hour. Presigned URLs last 5 minutes by default. Links that must live longer are managed share links, which never hand out a long-lived signature.
Strong authenticationYour identity provider's MFA applies. Cognito-only users must use TOTP MFA and passwords of at least 14 characters. User-existence errors are suppressed.
Hardened portalStrict CSP, HSTS, X-Frame-Options: DENY, Referrer-Policy: no-referrer, restrictive Permissions-Policy, no source maps (Portal).
Safe previewsOffice documents are converted server-side to sanitized HTML (no scripts, no external URLs) and shown in a sandboxed iframe; media and PDF are shown from short-lived presigned URLs.
AuditabilityApplication audit log, API access logs, Step Functions error logs, and CloudTrail in every account with the user's SourceIdentity.

Identity and authorization

The API trusts the Cognito ID token's signature, issuer and audience (checked by API Gateway), and then decides who the caller is from its claims. Roles, grants, job ownership and the bootstrap admin are keyed on the email claim, so a user who could pick their own email could take over someone else's access. The API therefore accepts the email only when it is one the user can't choose; otherwise it refuses the caller with 403 forbidden on every route.

How the user signs inEmail trusted whenWhy
SAML: IAM Identity Center, Entra ID, OktaalwaysThe email is asserted by your directory and signed by the IdP. Map it from an attribute users can't edit (Identity providers).
OIDCemail_verified is trueMany OIDC IdPs let users or guests set their email. The stack maps the IdP's verification claim to Cognito's email_verified.
Cognito user pool useremail_verified is trueUsers are created by administrators and can't change their email.

The provider is read from the signed token, never from anything the client sends.

Bootstrap admin. BootstrapAdminEmail is always an admin, so it is bound to an immutable identity: the first trusted sign-in with that email records the caller's Cognito user ID, and from then on only that user is the bootstrap admin. Sign in as the bootstrap admin straight after deployment. Resetting the binding is described in the admin guide.

Permissions, grants and session policies

What a user can do with an object is the intersection of two independent layers.

  1. The connection's IAM role sets the ceiling for everyone: the local role is limited by the stack parameters LocalBucketAccess and LocalAccessLevel; a spoke role by its AccessLevel and BucketScope. BucketDeck can never do more in an account than its role allows, and the account owner controls that role.
  2. BucketDeck grants decide what each user may do within that ceiling: a subject, a connection, scopes (a bucket, a bucket prefix or a bucket wildcard) and permissions (read, write, delete, manage, share). See the admin guide.

Grants are not only checked in code. For every request, the API and the worker call sts:AssumeRole on the connection's role with:

  • SourceIdentity = the user's email. It can't be changed for the rest of the session, also in role chains, and it appears in CloudTrail for every S3 call in the target account;
  • a role session name bd-<email>, also visible in CloudTrail;
  • a session policy (for non-admins) that allows only the S3 actions and bucket and prefix ARNs the user's grants allow. Even if the API had a bug in its own checks, AWS would refuse anything outside the grants. If a user's grants are too many to fit the session-policy size limit, the request is refused rather than widened.

The hub roles may assume only roles under the IAM path /bucketdeck/, and spoke roles trust them only with your external ID.

Access-key connections (only with AccessKeyConnections=Enabled) are the exception: static IAM user keys can't carry a session policy or SourceIdentity, so for them only BucketDeck's own checks and the IAM user's policies apply, and CloudTrail shows the IAM user. Prefer role connections.

Personal AWS access (users' own IAM Identity Center permissions or IAM user keys) is not governed by grants: AWS IAM alone decides what the user can do, and CloudTrail records the user's own identity. IAM user keys are exchanged for temporary credentials and never stored.

Jobs are checked against the owner's grants when they are created. The worker then makes the job's S3 calls with the owner's email as SourceIdentity, so CloudTrail attributes job traffic to the person who started it.

IAM summary

RoleAssumed byPermissions
API role (<prefix>-api-<region>)LambdaIts DynamoDB table; the data bucket; sts:AssumeRole, SetSourceIdentity and TagSession on roles under /bucketdeck/; Secrets Manager on <prefix>/connections/*; start and stop the job state machine; read the account's licences in AWS License Manager; KMS only through DynamoDB, S3 and Secrets Manager, or directly with a bucketdeck-* encryption context
Worker role (<prefix>-worker-<region>)LambdaAs the API role, but Secrets Manager read only and no Step Functions
Local access role (/bucketdeck/<prefix>-local-<region>)API and worker rolesThe S3 action set of LocalAccessLevel on the LocalBucketAccess buckets; s3:ListAllMyBuckets; CloudWatch storage metrics; KMS only through S3. Explicit deny of all S3 actions on BucketDeck's data and artifacts buckets
Spoke role (/bucketdeck/<RoleName>, other accounts)API and worker roles, with the external IDS3 ReadOnly, ReadWrite or Full action set on BucketScope; s3:ListAllMyBuckets; optional CloudWatch metrics; KMS only through S3
Job state machine roleStep FunctionsInvoke the worker; job files in the data bucket; its own Distributed Map executions; log delivery
Portal deployer roleLambdaDeploy the portal's Amplify app from one file in the artifacts bucket
Installer rolesECS (one-off task) and LambdaRun the installer task; read the account's licences; pull the image from the AWS Marketplace registry; write the code to the artifacts bucket

The S3 action sets:

SetActions
ReadOnlyListBucket, ListBucketVersions, ListBucketMultipartUploads, GetBucketLocation, GetBucketVersioning, GetEncryptionConfiguration, GetBucketPublicAccessBlock, GetBucketOwnershipControls, GetBucketObjectLockConfiguration, GetLifecycleConfiguration, GetReplicationConfiguration, GetBucketLogging, GetBucketTagging, GetBucketCORS, GetObject, GetObjectVersion, GetObjectAttributes, GetObjectVersionAttributes, GetObjectTagging, GetObjectVersionTagging, GetObjectRetention, GetObjectLegalHold
ReadWriteReadOnly plus PutObject, PutObjectTagging, PutObjectAcl, AbortMultipartUpload, ListMultipartUploadParts
FullReadWrite plus DeleteObject, DeleteObjectVersion, DeleteObjectTagging, PutObjectVersionTagging, RestoreObject, PutBucketCORS

No role has * resource access except where the AWS API has no resource-level permissions (for example s3:ListAllMyBuckets, License Manager and CloudWatch metrics reads), and KMS limited by kms:ViaService.

A presigned S3 URL can never outlive the credentials that signed it (at most one hour). Links that must last longer are managed share links (Team and above):

  • Only users with the share permission on the object can create them, and only up to the maximum lifetime an admin sets (7 days by default).
  • The link carries a random, unguessable secret. The table stores only its SHA-256 hash, so someone who reads the table can't rebuild a working link.
  • On every use, the API checks the secret, the expiry and the plan, rebuilds the creator's current permissions, and then redirects to a presigned URL valid for 60 seconds, signed with the creator's scoped credentials and SourceIdentity. A failed check returns a plain error without saying which check failed.
  • A link stops working when it expires, when it is revoked, or when its creator loses read on the object. BucketDeck keeps no list of users, so removing a user from the identity provider alone does not stop their links: remove their grants and role assignments too, or revoke the links.
  • Each use is counted and audited with the source IP; creation and revocation are audited too.

A share link is a bearer token: anyone who has it can download the object until the link stops working. Share only what may leave your organization, and prefer short lifetimes.

Data flows

FlowPathProtection
Sign-inBrowser ⇄ Cognito managed login ⇄ your IdPTLS; SAML signatures or OIDC; authorization code flow with PKCE
APIBrowser → API Gateway → LambdaTLS 1.2+; JWT on every call except share links; CORS limited to the portal origin; throttled
S3 callsLambda → STS → S3 in any connected accountTLS; per-request, per-user scoped STS credentials with SourceIdentity
Uploads, downloads, media previewsBrowser ⇄ S3 directlyTLS; presigned URLs signed with the user's scoped credentials, valid for minutes
Share linksRecipient → API Gateway → redirect → S3TLS; token checked against a stored hash; redirect to a 60-second presigned URL
TransfersWorker Lambda → S3 (server-side copy, or read and write when the accounts can't see each other)TLS; within your accounts
Job stateLambda ⇄ DynamoDB and the data bucketTLS; encrypted at rest
LicenceInstaller, API, worker → AWS License Manager in your accountTLS. A read of the account's licences for the product
Code installAWS Marketplace container registry → installer task → artifacts bucketImage pulled by digest; every file checked against a SHA-256 manifest; written to a private, TLS-only bucket

What stays in your account

Everything: identities, connections and keys, grants, listings and previews, job plans and reports, index snapshots and the audit log. Cloud Certainty operates no service in the data path, and the stack never contacts the vendor at runtime. The AWS Marketplace container registry and the vendor's public bucket in us-east-1 (templates and the release manifest) are read-only distribution points used at install and upgrade. Links to cloudcertainty.com in the portal are plain links, followed only when someone clicks them.

Encryption

  • In transit. TLS for every AWS API and the portal (HTTPS only, with HSTS). The data and artifacts buckets deny non-TLS requests.
  • At rest. Everything BucketDeck stores is encrypted with one customer managed KMS key: the key the stack creates (automatic rotation, retained with the table), or your own key (KmsKeyArn).
    • DynamoDB table: SSE-KMS with that key, plus item-level encryption (Data at rest).
    • Data and artifacts buckets: SSE-KMS with an S3 Bucket Key.
    • Access-key secrets (Secrets Manager) and CloudWatch Logs: the same key.
    • Your own buckets keep their own encryption settings. Copies use the destination's default encryption unless the job sets one.

Data at rest

Table encryption at rest alone is transparent: anyone allowed to read the table reads plaintext. So BucketDeck seals every item itself before writing it, and the table refuses changes from anyone else:

LayerWhat it gives
Item encryptionAll item content (connections, grants, roles, settings, jobs, share links, listings, Lens summaries, audit events, personal AWS sessions) is encrypted with AES-256-GCM under a per-table data key. Emails, connection IDs, bucket names and prefixes in keys are replaced by keyed hashes. With full DynamoDB access, a console, Scan or table export shows only ciphertext and hashes.
IntegrityEach ciphertext is bound to its item's key and expiry. An item changed, copied, moved or extended outside BucketDeck no longer opens, and BucketDeck refuses it. Not covered: putting back an older ciphertext of the same item, for example from a backup. The table's resource policy is the control against that; restore table backups only into a new table.
Key policyThe data key is wrapped by the KMS key. Only BucketDeck's API and worker roles may call KMS directly, and only with a bucketdeck-* encryption context. The created key's policy gives IAM administrators key administration but no cryptographic use, so IAM permissions alone (even AdministratorAccess) never decrypt the data. The data bucket (job reports, Lens snapshots, search index shards) and the access-key secrets are not item-encrypted: keep s3:GetObject on the data bucket and secretsmanager:GetSecretValue on <prefix>/connections/* to administrators.
Table resource policyDenies writes to every principal except the API and worker roles, whatever their IAM policies say, and denies DeleteTable to everyone. Deletion protection is on.

What remains, and how to cover it:

  • Atomic counters (job progress, share use counts) stay outside the ciphertext so DynamoDB can increment them. The resource policy protects them.
  • Whoever can change the key policy (kms:PutKeyPolicy) or the table's resource policy (dynamodb:PutResourcePolicy) could undo these controls. Both are visible in CloudTrail. Restrict them to your break-glass role with an SCP or permission boundary, and alert on them.
  • A restored or exported copy of the table stays encrypted, and BucketDeck can use a restored table as long as the same KMS key is available. Losing the key makes the data unreadable; BucketDeck then refuses to start rather than silently create a new key.

Portal hardening

Amplify Hosting serves the portal with these headers:

  • A strict Content-Security-Policy: no inline or third-party script, frame-ancestors 'none', and AWS endpoints allowed only for sign-in and for presigned previews, downloads and uploads.
  • Strict-Transport-Security, X-Content-Type-Options: nosniff, Referrer-Policy: no-referrer, X-Frame-Options: DENY, and a Permissions-Policy that turns off camera, microphone, geolocation, payment and USB.

Tokens are kept in session storage, never in cookies; refresh tokens last 8 hours and can be revoked.

Audit

Every administrative change, every connection and grant change, every job (start, cancel, retry, result), every share link (created, opened, revoked), upload, delete, rename and metadata change is written to the audit log with the actor, source IP and target. Admins read it in Administration → Audit. Events are kept for the stack's AuditRetentionDays, capped by the plan: 30 days on Starter, 90 on Team, 365 on Business and 3,650 on Enterprise. For each AWS call, CloudTrail in the account that owns the bucket shows the assumed-role session with sourceIdentity set to the user's email. Forward CloudTrail (with S3 data events if you need object-level detail) and the log groups to your SIEM.

Licensing

BucketDeck reads its AWS Marketplace licence from AWS License Manager in your account: at install, in the API and in the worker. Nothing about your usage or data is sent anywhere. Without a valid licence the installer stops, the API answers 402 licence_required and the worker starts no new jobs. If License Manager is unreachable, or a licence that was active disappears (which bridges an upgrade between plans), BucketDeck keeps working for up to 72 hours from the last successful check. Your data stays in place either way.

Shared responsibility

  • Keep BootstrapAdminEmail on a protected identity and enforce MFA at your IdP. Grant Admin to a second person or group.
  • Set spoke roles to the lowest AccessLevel and narrowest BucketScope that works; grants can narrow further but never widen them.
  • Prefer role connections; leave AccessKeyConnections=Disabled unless you need keys, and rotate keys you store.
  • On Business and above, set the IP allowlist to your corporate egress ranges if users should only connect from managed networks.
  • Enable CloudTrail S3 data events where you need object-level audit, and forward logs to your SIEM.

Something unclear or missing? Email [email protected].

Tell us what you have. We'll make it better and cheaper.

Send a short description of your current setup. We will tell you what we would improve, what it would cost, and where the savings are. A considered reply from an engineer, not a sales sequence.

TELL US ABOUT YOUR SETUP →