Security overview
This document is for security reviewers. It covers what Cloud Certainty TraceHound 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
TraceHound reads your CloudWatch Logs for many people, across accounts. What it must prevent:
| Threat | Mitigation |
|---|---|
| A user reads log groups no one granted them | Two independent layers: grants checked by the API on every call, and IAM credentials that a per-request session policy limits to the ARNs of the log groups the user may read. Logs Insights, FilterLogEvents, GetLogEvents and Live Tail are all evaluated by AWS against those credentials, so a query that names any other log group fails in AWS (see Permissions) |
| A user reads data masked by a data protection policy | The roles never have logs:Unmask, and session policies can't add it. Masked data stays masked for everyone, administrators included |
| A user reads someone else's query results | Each execution belongs to the person who started it: only they (and admins) can see its status, read its rows or cancel it, and reading the rows checks the user's read on every log group of the query again. Results are never reused across people |
| A shared snapshot leaks data | Snapshots exist only when someone creates one on purpose (the portal warns that everyone signed in can read it), need the export permission, are audited, are part of Team and above, and expire (30 days by default, at most what an admin allows) |
| Someone shortens retention and destroys evidence | Retention changes need the Team plan, a connection role with Manage, and a grant with manage; the session for each change allows PutRetentionPolicy and DeleteRetentionPolicy on that one log group only; every change is audited with the old and new values; the local role can't change the retention of TraceHound's own log groups, the audit log among them |
| A user runs up the CloudWatch bill | Per-user daily scan budgets (Business), a daily limit of live tail minutes per person, one shared Live Tail session per set of log groups, per-user result reuse, usage per user and Region, and the cost shown on every query |
| Someone impersonates another user | Federated 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 elsewhere | One-hour ID tokens, revocable 8-hour refresh tokens, optional IP allowlist checked on every call, the live tail stream included |
| The public live tail endpoint is abused | The function URL has no IAM authorisation, but the function validates the person's sign-in token itself and applies the same licence, IP allowlist and permission checks as the API (Streaming live tail) |
| TraceHound is used to change its own stack | The roles users act through have no IAM, CloudFormation, DynamoDB or S3 permissions, and the local role is denied TraceHound's own artifacts bucket |
| Actions can't be traced | Application audit log and query history with who ran what, plus SourceIdentity = <user email> on every assumed-role session, so CloudTrail in every account names the person behind each CloudWatch Logs call |
| Another account's role is assumed by a third party ("confused deputy") | Spoke roles trust only the hub role's ARN and require an external ID and a source identity |
| Data leaves your organisation | No vendor service in the data path; the stack never contacts the vendor at runtime |
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
| Principle | Control |
|---|---|
| Strong identity on every request | Every API route requires a Cognito ID token, validated by the API Gateway JWT authorizer (issuer and audience). The live tail function URL validates the same token itself. Admin routes check the role on each call. |
| Least privilege | Each component has its own role with resource-scoped permissions (IAM summary). Users act only through roles under the IAM path /tracehound/, narrowed per request to their grants. |
| Context-aware access | IP allowlist checked on every call (Business and above); outside it, every route except the sign-in check returns 403 ip_not_allowed. |
| Short-lived credentials | STS sessions of 15 minutes per user, connection and session policy. |
| Strong authentication | Your identity provider's MFA applies. Cognito-only users must use TOTP MFA and passwords of at least 14 characters. User-existence errors are suppressed. |
| Cost guard rails | Per-user daily scan budgets, a daily limit of live tail minutes, shared Live Tail sessions, per-user reuse. |
| Hardened portal | Strict CSP, HSTS, X-Frame-Options: DENY, Referrer-Policy: no-referrer, restrictive Permissions-Policy, no source maps (Portal). |
| Auditability | Query history with the person, application audit log, API access 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 by
the live tail function itself), and then decides who the caller is from its claims. Roles, grants,
history 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 in | Email trusted when | Why |
|---|---|---|
| SAML: IAM Identity Center, Entra ID, Okta | always | The email is asserted by your directory and signed by the IdP. Map it from an attribute users can't edit (Identity providers). |
| OIDC | email_verified is true | Many 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 user | email_verified is true | Users 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 two independent layers.
- The connection's IAM role sets the ceiling for everyone: the local role is limited by the
stack parameters
LocalAccessLevel(ReadorManage) andLogGroupPrefixes; a spoke role byAccessLevelandLogGroupPrefixesof its own stack. TraceHound can never do more in an account than its role allows, and the account owner controls that role. - TraceHound grants decide what each user may do within that ceiling: a subject (user or
group), a connection, Regions, log group name patterns and permissions (
browse,read,export,manage). See the admin guide.
Grants are not only checked in code. For every request, 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 CloudWatch Logs call in the target account;- a role session name
th-<email>, also visible in CloudTrail; - a session policy (for non-admins) scoped to the call:
| Used for | Allows |
|---|---|
| The shared log group list, the collector, the Lambda function list | logs:DescribeLogGroups, cloudwatch:GetMetricData, lambda:ListFunctions (AWS has no resource-level permissions for these; TraceHound filters the results by the caller's grants) |
| One log group's details | Its streams, subscription and metric filters, tags, data protection policy and anomaly detectors, on that log group's ARNs, and its CloudWatch metrics |
| Queries, searches, context, field lists, live tail | logs:StartQuery, FilterLogEvents, GetLogEvents, DescribeLogStreams, GetLogGroupFields and StartLiveTail only on the ARNs of the log group patterns the user may read; reading, stopping and listing queries (which have no resource-level permissions) |
| A retention change | logs:PutRetentionPolicy and DeleteRetentionPolicy on the one log group being changed (administrators get this scope too) |
If a user's grants are too many to fit the session policy size limit, the request is refused rather
than widened. Grant patterns use the same matching as IAM (* any run of characters, ? one,
case-sensitive), so what TraceHound shows and what AWS allows agree.
Your SCPs, permissions boundaries and resource policies still apply on top: TraceHound adds restrictions and never removes yours.
Streaming live tail
Browsers can't carry CloudWatch Logs Live Tail's event stream through API Gateway, so a separate
Lambda function serves it through a Lambda function URL with response streaming (stack output
LiveTailUrl).
- The function URL has auth type
NONEbecause browsers can't sign requests with IAM. The function itself validates the Cognito ID token on every request (signature against the user pool's keys, issuer, audience, token use and expiry), then applies the same checks as the API: trusted email, licence, IP allowlist, andreadon every log group requested (names only, at most 10). A request that fails any check gets no data. - CORS allows only the portal's origin. Each stream ends after about 14 minutes and the browser reconnects, which repeats every check.
- AWS calls use the same per-user credentials and session policy as the API, limited to the log groups the user may read.
- Sharing between viewers. To limit Live Tail cost, people who tail the same log groups with the same filter share one Live Tail session: one stream runs it and writes the events to the shared cache, and the others read them from there. Each viewer's permissions are checked before they can join. The events are kept in the cache for about two minutes; DynamoDB removes expired items in the background, and expired items are never served.
IAM summary
| Role | Assumed by | Permissions |
|---|---|---|
Hub role (<NamePrefix>-api-<Region>) | The API, live tail and collector Lambdas | Its two DynamoDB tables; sts:AssumeRole, SetSourceIdentity and TagSession on roles under /tracehound/ only; read the account's licences in AWS License Manager; invoke the collector; its log groups; KMS only through DynamoDB. No CloudWatch Logs access of its own |
Local role (/tracehound/<NamePrefix>-local-<Region>) | The hub role, with a source identity (an email, or tracehound-collector) | List log groups, metric filters and anomaly detectors, read CloudWatch metrics, list Lambda functions, read, stop and list queries (*, no resource-level permissions); on log groups of this account (or under LogGroupPrefixes): streams, subscription filters, tags, data protection policy, StartQuery, FilterLogEvents, GetLogEvents, GetLogGroupFields, StartLiveTail. Manage adds PutRetentionPolicy and DeleteRetentionPolicy. Explicit denies: log group actions outside this account, every S3 action on TraceHound's artifacts bucket, and retention changes of TraceHound's own log groups. Never logs:Unmask; no IAM, CloudFormation, DynamoDB or S3 |
Spoke role (/tracehound/<RoleName>, other accounts) | The hub role, with the external ID and a source identity | As the local role, by its AccessLevel and LogGroupPrefixes (without the denies that name the hub's own resources) |
| Update notifier role | Lambda | Its settings and audit items; publish to the update topic |
| Portal deployer role | Lambda | Deploy the portal's Amplify app from one file in the artifacts bucket |
| Installer roles | ECS (one-off task) and Lambda | Run the installer task; read the account's licences; pull the image from the AWS Marketplace registry; write the code to the artifacts bucket |
Data flows
| Flow | Path | Protection |
|---|---|---|
| Sign-in | Browser ⇄ Cognito managed login ⇄ your IdP | TLS; SAML signatures or OIDC; authorization code flow with PKCE |
| API | Browser → API Gateway → Lambda | TLS 1.2+; JWT on every call; CORS limited to the portal origin; throttled |
| Queries and events | Lambda → STS → CloudWatch Logs in any connected account | TLS; per-request, per-user scoped STS credentials with SourceIdentity |
| Results on screen | CloudWatch Logs GetQueryResults → Lambda → browser | TLS; the owner (or an admin) only, with read checked again; up to 10,000 rows |
| Live tail | Browser → live tail function URL → CloudWatch Logs Live Tail | TLS; the ID token validated by the function; per-user scoped STS credentials; CORS limited to the portal |
| Storage history | Collector → STS → CloudWatch Logs and CloudWatch metrics → DynamoDB | TLS; a list-and-metrics-only session with the source identity tracehound-collector |
| Licence | Installer and API → AWS License Manager in your account | TLS. A read of the account's licences for the product |
| Code install | AWS Marketplace container registry → installer task → artifacts bucket | Image pulled by digest; every file checked against a SHA-256 manifest; written to a private, TLS-only bucket |
The shared cache
The log group list is slow and rate-limited to read, so TraceHound keeps it in a second DynamoDB
table shared by every user: each Region's log groups (names, sizes and settings, no events), field
names per log group, Lambda function names, the live tail session events (about two minutes) and the
daily usage counters. Every read is filtered by the caller's grants exactly as a live call would be:
field names only for callers with read on that log group. It never holds query results. It is
reachable only by the hub role, encrypted like the main table, and can be rebuilt at any time.
What stays in your account
Everything: identities, connections and external IDs, grants, the query history with query text,
storage history, saved queries, dashboards, snapshots (which contain result rows), usage figures
and the audit log, as well as your logs and the query results, which stay in CloudWatch Logs.
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,
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.
Encryption
- In transit. TLS for every AWS API, the portal and the live tail stream (HTTPS only, with HSTS). The artifacts bucket denies non-TLS requests.
- At rest. With
KmsKeyArn, your customer managed key encrypts the DynamoDB tables, TraceHound's own CloudWatch log groups, the update topic and the artifacts bucket. Without it, AWS owned keys and SSE-S3 are used. Your log groups keep their 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, the API and the live tail function URL in the stack's Region. 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. CSV and TSV exports defuse spreadsheet formulas.
Audit
Every administrative change (roles, connections, grants, settings), every retention change (with the
old and new values), every full result export, every collector run started by hand, every snapshot
created or deleted, every saved query and dashboard change, and refused requests 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, and are also written to TraceHound's CloudWatch log groups. The query history keeps who
ran every query, and what it scanned and cost, for 30 to 730 days by plan. In each account,
CloudTrail shows the assumed-role session behind each call with sourceIdentity set to the user's
email. Forward CloudTrail and the log groups to your SIEM.
Licensing
TraceHound 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), TraceHound keeps working for up to
72 hours from the last successful check. Your data stays in place either way.
Shared responsibility
- Keep
BootstrapAdminEmailon a protected identity and enforce MFA at your IdP. Grant Admin to a second person or group. - Keep connection roles at
Readunless people should change retention through TraceHound, and setLogGroupPrefixeswhere TraceHound should read only some log groups of an account. - Grant groups rather than single users, with log group prefixes per team (
/aws/lambda/payments-*). - Use data protection policies for sensitive fields in your logs; TraceHound never unmasks them.
- Set a daily live tail minute limit and, on Business, daily scan budgets, and review Administration › Usage.
- Treat snapshots and exports like copies of your logs: share only data everyone signed in may see, with short expiries.
- On Business and above, set the IP allowlist to your corporate egress ranges if users should only connect from managed networks.
- Forward CloudTrail and TraceHound's log groups to your SIEM.
Something unclear or missing? Email [email protected].