Security overview
This document is for security reviewers. It covers what Cloud Certainty SQLark 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
SQLark runs Amazon Athena queries over your data for many people. What it must prevent:
| Threat | Mitigation |
|---|---|
| A user queries tables 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 granted workgroup and the AWS Glue databases granted in it. Athena reads the catalogue with those credentials, so a query that touches any other database fails in AWS (see Permissions) |
| A user reads someone else's results | Each execution belongs to the person who started it: only they (and admins) can see its status, read its rows, cancel it or download its file. 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 download permission, are audited, are part of Team and above, and expire (30 days by default, at most what an admin allows) |
| Someone changes data or tables with a query | Statements other than plain reads need the ddl permission, the Team plan and a connection role with Full. Even if the check in the API were fooled, the AWS session of a user without ddl has no Glue write permissions and can write to S3 only at the workgroup's result location. Every write statement is audited |
| A user runs up the Athena bill | A per-query data scanned limit on SQLark workgroups (1 TB by default), per-user daily scan budgets (Business), usage per user and workgroup, 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 |
| SQLark is used to change its own stack | The roles users act through have no IAM, CloudFormation or DynamoDB permissions, and are denied SQLark'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 Athena, Glue and S3 call |
| Another account's role is assumed by a third party ("confused deputy") | Spoke roles trust only the hub's API role 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). 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 /sqlark/, 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 and connection. Result downloads are presigned links for one file, valid for at most 15 minutes. |
| 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-query scan limit on SQLark workgroups, enforced workgroup settings, per-user daily scan budgets. |
| 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
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(QueryorFull),DataBucketNames,ResultBucketNamesandDataKmsKeyArns; a spoke role by the same parameters of its own stack. SQLark can never do more in an account than its role allows, and the account owner controls that role. - SQLark grants decide what each user may do within that ceiling: a subject (user or group), a
connection, Regions, workgroup patterns, database patterns and permissions (
browse,query,download,ddl). 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 Athena, Glue and S3 call in the target account;- a role session name
sq-<email>, also visible in CloudTrail; - a session policy (for non-admins) that allows only the Athena actions on the one workgroup the
request needs, AWS Glue reads only on the databases (and their tables) granted in that workgroup,
Glue writes only where the user holds
ddl, and S3 writes only to the workgroup's result location (plus the data locations withddl). Athena uses these same credentials to read the catalogue, the table data and KMS keys, so the policy bounds the query itself. If a user's grants are too many to fit the session policy size limit, the request is refused rather than widened.
Grants on workgroup A with database X and on workgroup B with database Y never combine into "B with X": each execution's policy holds only the databases granted in its own workgroup.
Your Lake Formation permissions, bucket policies and SCPs still apply on top: SQLark adds restrictions and never removes yours. Use Lake Formation for row and column security inside a table.
IAM summary
| Role | Assumed by | Permissions |
|---|---|---|
API role (<prefix>-api-<region>) | Lambda | Its two DynamoDB tables; sts:AssumeRole, SetSourceIdentity and TagSession on roles under /sqlark/ only; read the account's licences in AWS License Manager; its log group; KMS only through DynamoDB. No Athena, Glue or S3 of its own |
Local role (/sqlark/<prefix>-local-<region>) | API role, with a source identity | Athena: list workgroups, catalogues and engine versions; run, read, stop and inspect queries in workgroups; read the AwsDataCatalog catalogue. AWS Glue reads. lakeformation:GetDataAccess. S3 read on DataBucketNames; read and write on SQLark result buckets owned by the account and on ResultBucketNames. KMS on DataKmsKeyArns and, through S3, the stack's key. Full adds Glue database, table and partition writes and S3 write and delete on DataBucketNames. Explicit deny of all S3 actions on SQLark's artifacts bucket |
Spoke role (/sqlark/<RoleName>, other accounts) | API role, with the external ID and a source identity | As the local role, by its stack's parameters |
| 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 |
* resources appear only where AWS has no resource-level permissions (athena:ListWorkGroups,
ListDataCatalogs, ListEngineVersions, License Manager, lakeformation:GetDataAccess) and for
DataBucketNames when you keep its default *. For the listings, SQLark filters results by the
caller's grants: a user never sees a workgroup or database they have no grant on.
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 | Lambda → STS → Athena in any connected account; Athena → Glue, S3 and KMS with the same scoped session | TLS; per-request, per-user scoped STS credentials with SourceIdentity |
| Results on screen | Athena → Lambda → browser | TLS; the owner (or an admin) only; up to 50,000 rows per request |
| Result downloads | Browser → S3 directly | TLS; a presigned link for one file, signed with the user's scoped session, valid for at most 15 minutes; audited |
| 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 |
What stays in your account
Everything: identities, connections and external IDs, grants, the query history with SQL text,
saved queries, dashboards, snapshots (which contain result rows), usage figures and the audit log,
as well as your data and the query results. 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 and the portal (HTTPS only, with HSTS). The artifacts and results buckets deny non-TLS requests.
- At rest. With
KmsKeyArn(hub) orResultsKmsKeyArn(spoke), your customer managed key encrypts the DynamoDB tables, CloudWatch Logs, the update topic, the artifacts bucket and the SQLark results buckets, and the SQLark workgroup enforces SSE-KMS for its results. Without it, AWS owned keys and SSE-S3 are used. Your own buckets, tables and catalogue keep their own encryption. - Query results. SQLark results buckets block public access, enforce bucket ownership, are
reachable only over TLS and delete results after
ResultsRetentionDays(30 by default).
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. Downloads open the presigned S3 link as a normal navigation. 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), every write statement, every
result download, every snapshot created or deleted, and every saved query and dashboard 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 plan's audit days: 30 on Starter, 90 on Team,
365 on Business and 3,650 on Enterprise. The query history keeps who ran every query for 30 to 730
days by plan. For each AWS call, CloudTrail in the account that runs the query shows the
assumed-role session with sourceIdentity set to the user's email. Forward CloudTrail and the log
groups to your SIEM.
Licensing
SQLark 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), SQLark 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
Queryunless a team needs write statements there, and narrowDataBucketNamesto your data lake buckets. - Grant groups rather than single users, with database prefixes per team (
sales_*) in team workgroups. - Keep the per-query scan limit on SQLark workgroups, set scan budgets on Business, and review Administration › Usage.
- Treat snapshots like exports: 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 SQLark's log groups to your SIEM.
Something unclear or missing? Email [email protected].