← ALL DOCS
DOCS · SECURITY REVIEWERS

Security overview

This document is for security reviewers. It covers what Cloud Certainty Watchtower 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.

Watchtower reads AWS Security Hub findings across your organisation, writes workflow changes back when you allow it, and, when you configure it, reads your GitHub repositories and opens pull requests in them. Its controls make sure that each person sees and changes exactly the findings an administrator granted them, that AWS itself bounds what each session can do, that tokens for code hosts never leak, that changes to code always go through a pull request a person reviews, and that every action can be traced to a person.

Threat model

ThreatMitigation
A user sees findings of accounts nobody granted themEnforced by Watchtower on every request: every search, count, facet, issue, Overview, export and detail is computed from the findings whose connection, account and Region match the caller's grants, and a finding outside them answers "not found" (see Account grants and Security Hub)
A user changes findings they may only readtriage is a separate permission, checked for every finding in a request. The AWS session of a user without triage carries a session policy without securityhub:BatchUpdateFindings, and a connection whose owner chose Read has a role without it, so AWS refuses the change even if Watchtower's own check had a bug
Watchtower is used to change AWS resourcesConnection roles have no write permission except securityhub:BatchUpdateFindings (workflow status, note, severity), and only with Triage. No IAM, CloudFormation, DynamoDB or S3 permissions in your accounts. Fixes change code, never infrastructure
A fix pull request does something unexpectedFixes are a fixed list of small text edits per control, tested against known files. The patch is recomputed against the branch head, committed only to a new branch named watchtower/fix-..., and proposed as a pull request. Nothing is merged or deployed by Watchtower; your review and pipeline decide. Needs the fix permission and the Business plan; every pull request is audited
A GitHub or SonarCloud token leaksTokens and GitHub App private keys are write-only in the portal, stored in AWS Secrets Manager encrypted with the stack's key, read only by the API, the sync worker and the scanner task, and never returned, logged or written to the audit log. Error messages from code hosts are redacted. GitHub App installation tokens last at most an hour
A forged SARIF uploadThe upload route is the only one without a user sign-in. It needs an ingest token scoped to the repository, unexpired and unrevoked; Watchtower stores only its SHA-256 hash. Uploads are throttled, size-limited (4 MB as sent, 50 MB decompressed, 50,000 results) and audited. Uploaded findings only inform; they change nothing in AWS or GitHub
A malicious repository attacks the scannerArchives are extracted with path checks (no absolute paths, .., links or devices) and size caps into the task's ephemeral storage, with a read-only root filesystem and all Linux capabilities dropped. The task role can read only the integration secrets and write only scan results. Its security group allows no inbound traffic and only outbound HTTPS. The task is discarded after each scan
An exception hides a risk foreverExceptions need a reason and an expiry (at most a year by default), need triage over their whole scope, are listed for everyone, are audited, and end on their own: the findings come back as NEW with a note
A third party assumes a spoke role ("confused deputy")Spoke roles trust only the hub's API and sync role ARNs, and require your external ID and a source identity
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
Watchtower is used to change its own stackConnection roles have no IAM, CloudFormation or DynamoDB permissions, and the local role is explicitly denied every S3 action on Watchtower's own buckets
Actions can't be tracedApplication audit log, plus SourceIdentity = <user email> on every assumed-role session, so CloudTrail in the target account names the person behind each Security Hub call. Notes written to Security Hub carry the user's email as their author
Data leaves your organisationNo vendor service in the data path and no telemetry. The stack contacts only AWS, the code hosts you configure, and optionally the vendor's public release manifest

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

Zero-trust controls

PrincipleControl
Strong identity on every requestEvery API route requires a Cognito ID token, validated by the API Gateway JWT authorizer (issuer and audience), except the SARIF upload route, which requires an ingest token. 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 /watchtower/, narrowed per request by a session policy.
Never trust the networkNo inbound connections to anything in your VPC. Optional IP allowlist (Business and above) checked on every call; outside it, every route except the sign-in check returns 403 ip_not_allowed.
Short-lived credentialsSTS sessions of 15 minutes per user and connection. GitHub App installation tokens of at most an hour.
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.
Changes need a personFindings change only through a person with triage; code changes only through a pull request a person merges; exceptions expire.
Hardened portalStrict CSP, HSTS, X-Frame-Options: DENY, Referrer-Policy: no-referrer, restrictive Permissions-Policy, no source maps (Portal).
AuditabilityApplication audit log, API access logs, and CloudTrail in every connected account with the user's SourceIdentity.

Identity and authorisation

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, tasks 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 is the intersection of:

  1. The connection's IAM role, set by the account owner: LocalAccessLevel on the hub stack, AccessLevel on each spoke stack. Read allows Security Hub reads; Triage adds securityhub:BatchUpdateFindings. Watchtower can never do more in an account than its role allows.
  2. Watchtower grants: a subject (user or group), a connection (or all), AWS account patterns (*, ?), Regions (or all), and the permissions view, triage (implies view) and fix (implies view). Administrators hold every permission, still bounded by the role. See the admin guide.

For every request that reaches Security Hub, the API calls 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 Security Hub call in the target account (the sync worker uses watchtower-sync);
  • a role session name wt-<email>, also visible in CloudTrail;
  • the external ID for spoke roles;
  • a session policy (for non-admins) for the scope of the call:
ScopeAllows
ReadFinding detail and history: securityhub:GetFindings, GetFindingHistory, DescribeHub, standards and controls, finding aggregators
TriageRead plus securityhub:BatchUpdateFindings, only for users with triage
SyncRead plus organizations:ListAccounts and DescribeOrganization (account names), for the sync worker

Account grants and Security Hub

AWS Security Hub has no IAM permissions per member account. GetFindings and BatchUpdateFindings act on the hub (arn:aws:securityhub:<region>:<account>:hub/default) of the account they are called in: in the delegated administrator, they read or change the findings of every member account. No IAM policy or session policy can say "only the findings of account 111122223333".

So the two layers do different jobs:

  • Watchtower enforces which findings each person may see and change. The index holds every finding of a source and is filtered to the caller's grants on every read; every finding in a change request is checked against the caller's triage grants before anything is sent; findings outside the grants answer "not found". This is covered by tests on every route.
  • AWS enforces which actions each session may take. A user without triage gets a session that AWS refuses to let change any finding; a Read connection's role can't change findings at all; and no role can do anything outside Security Hub reads, BatchUpdateFindings and the Organizations account list.

If you need AWS itself to separate accounts, connect separate Security Hub administrators (for example per business unit) as separate connections with their own roles.

IAM summary

RoleAssumed byPermissions
API role (<prefix>-api-<region>)API LambdaIts two DynamoDB tables; the data bucket; sts:AssumeRole, SetSourceIdentity and TagSession on roles under /watchtower/ only; invoking the sync worker; Secrets Manager on <prefix>/integrations/* only; ecs:RunTask on the scanner task definition, with iam:PassRole on the scanner's two roles only; reading the account's licences in AWS License Manager; its log group; KMS only through DynamoDB, S3 and Secrets Manager. No Security Hub access of its own
Sync role (<prefix>-sync-<region>)Sync worker LambdaAs the API role, plus invoking itself. No Security Hub access of its own
Local role (/watchtower/<prefix>-local-<region>)API and sync roles, with a source identitySecurity Hub reads; organizations:ListAccounts and DescribeOrganization; with Triage, securityhub:BatchUpdateFindings on this account's hub. Explicit deny of all S3 actions on Watchtower's artifacts and data buckets. No IAM, CloudFormation or DynamoDB
Spoke role (/watchtower/<RoleName>, other accounts)API and sync roles, with the external ID and a source identityAs the local role, by the spoke stack's AccessLevel
Scanner task roleScanner task (Fargate)Read the integration secrets; read and write scans/ and tmp/ in the data bucket; write scan run items in the main table only; its log group
Update notifier roleLambdaIts settings and audit items; publish to the update topic
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

* resources appear only where AWS has no resource-level permissions (Security Hub reads, AWS Organizations, License Manager).

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; CORS limited to the portal origin; throttled
Findings syncSync worker → STS → Security Hub in the hub or delegated administrator account → data bucketTLS; SourceIdentity = watchtower-sync; Sync session policy; private, TLS-only bucket
Finding detail and triageLambda → STS → Security HubTLS; per-request, per-user scoped STS credentials with SourceIdentity
CodeLambda, sync worker and scanner task → GitHub or GitHub Enterprise ServerTLS; token from Secrets Manager; responses size-capped
SonarCloud, SonarQubeSync worker → SonarCloud or your SonarQubeTLS; token from Secrets Manager
SARIF uploadCI → API Gateway → LambdaTLS; ingest token; size limits; throttled
LicenceInstaller and API → 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 external IDs, grants, the findings index, posture and trends, tasks and comments, exceptions, saved views, code indexes (resource names, types, tags and line numbers, not whole files), scan results, integration secrets, usage figures and the audit log. File contents are read from GitHub when someone opens a file or previews a fix, and are not stored.

Cloud Certainty operates no service in the data path, and Watchtower sends no telemetry. 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, and, with UpdateChecks = Enabled, for a daily read of the public release manifest that sends nothing about you. Links to cloudcertainty.com in the portal are plain links, followed only when someone clicks them.

Code hosts

The only places Watchtower sends data outside AWS are the code hosts you configure:

  • GitHub or GitHub Enterprise Server. Watchtower reads repository trees, files and archives. When a person opens a fix pull request or creates a GitHub issue, Watchtower writes a branch, commits and a pull request, or an issue, whose text includes the finding titles, AWS account ids and resource ids concerned (never notes). Choose repositories whose visibility suits that information.
  • SonarCloud or SonarQube. Watchtower only reads issues and hotspots of the projects you map.

Use a GitHub App or fine-grained tokens limited to the repositories Watchtower indexes, with write permissions only for the features you use (Code integrations).

Encryption

  • In transit. TLS for every AWS API, every code host and the portal (HTTPS only, with HSTS). The artifacts and data buckets deny non-TLS requests.
  • At rest. With KmsKeyArn, your customer managed key encrypts the DynamoDB tables, CloudWatch Logs, the update topic, the artifacts and data buckets and the integration secrets. Without it, AWS owned or AWS managed keys and SSE-S3 are used. Security Hub keeps its own encryption.

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 the API.
  • 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.

Charts are drawn in the browser; nothing is loaded from third parties. Tokens are kept in session storage, never in cookies; refresh tokens last 8 hours and can be revoked.

Audit

Every administrative change (roles, connections, grants, settings, integrations, repositories, ingest tokens), every change to findings in Security Hub, every task created or deleted, every exception created, changed, revoked or applied, saved views, fix pull requests, GitHub issues, scans, SARIF uploads and manual syncs are written to the audit log with the actor, source IP and target. Admins read it in Administration › Audit log. Events are kept for the plan's audit days: 30 on Starter, 90 on Team, 365 on Business and 3,650 on Enterprise. For each AWS call, CloudTrail in the connected account shows the assumed-role session with sourceIdentity set to the user's email. Forward CloudTrail and Watchtower's log groups to your SIEM.

Licensing

Watchtower reads its AWS Marketplace licence from AWS License Manager in your account: at install and in the API. Nothing about your usage or data is sent anywhere. Without a valid licence the installer stops and the API answers 402 licence_required. If License Manager is unreachable, or a licence that was active disappears (which bridges a move between plans), Watchtower 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.
  • Keep connection roles at Read unless your team triages in Watchtower, and grant triage to the people who own each account.
  • Grant groups rather than single users, with account patterns per team.
  • Prefer a GitHub App installed on the infrastructure repositories only, or fine-grained tokens limited to them. Protect default branches so that Watchtower's pull requests need review.
  • Review exceptions when they expire, and keep their maximum length short.
  • Scope each ingest token to one repository, give it an expiry, and rotate it.
  • On Business and above, set the IP allowlist to your corporate egress ranges if users should only connect from managed networks.
  • Forward CloudTrail and Watchtower's log groups 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 →