Security model
Threat model, encryption and keys, authorization, isolation and abuse limits.
Secure File Transfer is designed on the assumption that links get forwarded, credentials leak and files are hostile. This page summarises the threat model, encryption and keys, authentication, tenant isolation, abuse limits and the supply chain.
Threat model
The threat model applies STRIDE (spoofing, tampering, repudiation, information disclosure, denial of service, elevation of privilege) to each element of the data-flow diagram and to each key flow: send, verify and download, request, revoke and expire. It covers the application, every data store, the edge, the Osaka standby, the delivery pipeline, the AWS accounts and their operators, in dev and staging as well as prod.
- Stable IDs. Threats are numbered T-01 to T-107 and never renumbered. Each names its mitigation and the requirement or ADR it relies on.
- Rated twice. Likelihood and impact are rated without mitigation, then the residual risk with it. An element with a High residual risk doesn't ship.
- Accepted risks in writing. Eighteen residual risks (R-01 to R-18) are accepted, each with an owner, a review date and a trigger to revisit early. Examples: mailbox control is the only factor for external recipients (R-02), signature scanning misses new malware (R-04), and one maintainer means no separation of duties (R-08).
- A test per mitigation. Each mitigation group maps to an automated test, a CI check or a drill. The model is reviewed in every pull request that changes a trust boundary and in full every six months.
Encryption and keys
All traffic uses TLS 1.2 or later, including from the load balancer to the API pods. At rest, each environment has eight customer managed KMS keys, one per data class (ADR-0017). Each is a multi-Region key with its primary in Tokyo and, in staging and prod, a replica in Osaka, rotated every year.
| Key | Protects |
|---|---|
| files | S3 files buckets; scan node volumes |
| fields | Per-tenant data keys for file names, messages and DLP finding details |
| database | Aurora clusters, backups and snapshots |
| audit | The audit archive, which outlives teardown |
| logs | Log buckets, CloudWatch Logs, log and trace stores |
| secrets | Secrets Manager and Kubernetes Secrets |
| platform | SQS, SNS, Valkey and EBS |
| tfstate | Terraform state buckets |
A ninth key, in the shared account, encrypts container images. Load balancer access logs are the one exception NFR-13 allows: they support only S3-managed encryption, and they never contain link tokens.
Field encryption. File names and messages are encrypted in PostgreSQL with AES-256-GCM under a data key per tenant. The service unwraps the key of the tenant in the caller's scope, never the tenant on the row, so a row read across tenants fails to decrypt and raises an alert.
Why not a KMS key per tenant. The API must serve every tenant, so its role could use every tenant's key anyway; per-tenant keys wouldn't protect against a compromised service. For 500 tenants in two regions they would cost about $1,000 to $3,000 a month as keys rotate, and add a privileged path to provisioning. A dedicated key, ours or held in the tenant's own AWS account, is the opt-in path for a tenant whose contract requires one. Service control policies stop anyone but the deploy roles, the bootstrap admin role and a break-glass key-admin role from disabling or deleting a key or changing its policy.
Authentication and authorization
Each caller type has one credential, and a request that carries two is refused.
| Caller | Proves identity with | Session |
|---|---|---|
| Member or tenant admin | Cognito with mandatory MFA (TOTP or a passkey), or the tenant's identity provider by SAML or OIDC | Server-side session in a __Host- cookie: 12 hours, 30 minutes idle. No Cognito token reaches the browser |
| Recipient or uploader | The emailed link token, then a 6-digit code sent to the invited address | Link session: 24 hours, 1 hour idle, never past the transfer's expiry |
| Tenant automation | A tenant API key with fixed scopes, as a Bearer token | Expires after 90 days by default, at most 365 |
| Platform operator | IAM Identity Center with MFA by security key or passkey | A 15-minute operator token, through a port-forward |
Admin rights for a local admin are designed to require a passkey session, and sensitive actions such as creating an API key need a sign-in within the last 15 minutes. API keys never download file contents, decide DLP approvals or manage users. Operators can't read tenant data or file contents; break-glass access is time-limited, audited and raises an alert. See Recipient verification and Administration.
Tenant isolation
NFR-12 is absolute: no API call, link or token gives access to another tenant's data. It is enforced in layers, so one bug is not a breach:
- scoped data access in the application, which refuses a query without a scope;
- composite foreign keys that include
tenant_id; - PostgreSQL row-level security on every tenant-owned table, which no workload role can bypass (ADR-0020);
- IAM conditions on S3 object prefixes and scan tags;
- per-tenant data keys for names and messages.
An item in another tenant returns 404, never 403, so its existence is never revealed. The isolation suite tries every endpoint and data-access function with a second tenant's IDs, tokens and keys. It runs on every pull request, and is planned to run nightly against staging.
Abuse limits
NFR-14 sets the starting limits. Counters live in Valkey, with WAF rate rules in front for floods. The state that decides access, such as code attempts, lockouts and download counts, stays in PostgreSQL.
| Limit | Starting value |
|---|---|
| Failed member sign-ins | 10 per account per 15 minutes |
| Verification codes | 5 per address per hour and 50 a day; 20 per link a day |
| Code validity | 10 minutes, 5 attempts |
| Lockout | After 20 failed attempts, until the member unlocks |
| Link-page requests | 60 per IP address per minute |
| API requests | 1,000 per tenant per minute |
| Downloads granted | 500 GB per tenant and 25 GB per recipient a day |
| Uploads | 200 GB declared per tenant a day; 2 TB stored per tenant |
| API key audit export | Once an hour, at most 100,000 events |
A verified session lasts at most 24 hours, and link tokens carry 256 bits of randomness. If the counter store is down, verification and sign-in fail closed.
Secrets and supply chain
Runtime secrets live in AWS Secrets Manager, never in git. Workloads use IAM roles and IAM database authentication, so there are no static database passwords and no long-lived AWS keys. GitHub Actions reaches AWS through OIDC roles pinned to this repository.
| Control | Where |
|---|---|
| Secret scanning on every commit and pull request | gitleaks in pre-commit and CI |
| Vulnerability and misconfiguration scans of code, images and Terraform | Trivy in CI; no critical or high finding in a released image |
| Pinned dependencies | Hashed lockfiles; Actions pinned to commit SHAs; weekly Dependabot with a cooldown |
| Signed images with SBOM and provenance | cosign keyless signing through GitHub OIDC |
| Only signed images run | Kyverno admits an image only by digest, with our signature or an allow-listed vendor's |
| Hardened containers | Non-root, read-only filesystem, no capabilities |
Scanning, pinning and signing run in CI today, and each release publishes a signed image with an SBOM to GitHub Container Registry. Admission control arrives with the clusters, and Amazon ECR in the shared account replaces GitHub's registry once that account exists (ADR-0007).
Verification
The baseline (NFR-13) targets OWASP ASVS 5.0 Level 2 for the web app and API; the threat model holds the checklist. An ASVS review and a penetration test of the public surface are planned before prod opens, then yearly. Report vulnerabilities privately through GitHub's private vulnerability reporting.
Source: Threat model, Encryption and keys, Authentication and authorization, Abuse and rate limits, SECURITY.md, PRD