White Paper ← All white papers

GrandLine Architecture Intelligence. Security White Paper

Revision: 2026-Q2 · Audience: security architects, CISOs, compliance leads evaluating GrandLine Architecture Intelligence for multi-cloud architecture, security, and FinOps.

This is an engineering-honest document. If a control is partial, we say so. If a control is on the roadmap, we say so. If a question is unanswered here, ask us and we will answer it in writing.

1. Summary

GrandLine Architecture Intelligence is a multi-tenant SaaS (and optional self-hosted) platform that ingests read-only metadata from AWS, Azure, and GCP, builds a normalised inventory and relationship graph, renders architecture diagrams, runs security and cost rules against the graph, and publishes reports. GrandLine is a metadata system: we do not proxy production traffic, we do not read object data, we do not handle payment card data, and we do not hold workload secrets for our customers.

The primary security claims are:

  1. Tenant isolation is defence-in-depth, not a single control: logical (tenant-scoped IDs), database (Postgres RLS), cryptographic (per-tenant AWS KMS customer managed keys for report objects and connector secrets), and network (per-tenant S3 prefixes + scoped IAM on the SaaS control plane).
  2. Read-only by design for every cloud connector. AWS uses AssumeRole with an ExternalId; Azure uses Microsoft Entra ID Workload Identity Federation (federated credential on an app registration) granted Reader; GCP uses Workload Identity Federation to a service account with roles/viewer, roles/iam.securityReviewer, and roles/bigquery.dataViewer. We publish the IAM policies we use and diff them per release.
  3. Least privilege inside the product too. The API enforces ABAC on every query. Role assignments are scoped to tenant, account, or a resource tag.
  4. Audit is immutable and exportable. Every privileged action writes to an append-only log with a SHA-256 chain and an optional S3 Object Lock vault (7 years, Governance mode).
  5. Supply chain is attested. Every container we ship is built with hermetic builders, produces an SBOM (CycloneDX), and is signed with Sigstore Cosign. Customers can verify before running.

This paper explains each of these claims with specifics.


2. Threat model

We model four adversary classes:

A1. External attacker on the internet. Network unauthenticated. Goal: extract tenant data, pivot into customer clouds, take over the control plane.

A2. Malicious or compromised tenant user. Authenticated to tenant T. Goal: read or modify data belonging to tenant T' ≠ T; escalate from analyst to admin within T.

A3. Malicious GrandLine operator (insider). Has production access. Goal: read any tenant's data, tamper with findings, exfiltrate secrets.

A4. Supply chain attacker. Compromise of a dependency, CI, or container registry. Goal: ship backdoored code that runs in customer environments.

Out of scope in this paper: nation-state attacks with lawful access to cloud infrastructure (we document data residency separately), physical attacks on Anthropic / AWS datacentres, and compromise of the customer's IdP (we cannot defend session tokens we did not issue).

For each class we identify the primary controls:

Adversary Primary mitigations
A1WAF + CloudFront + rate-limiting; TLS 1.3 only; CSP + SRI; short-lived JWTs with rotating keys; MFA required for all human accounts; login-anomaly detection.
A2Tenant-scoped IDs on every table; Postgres RLS enforced at the DB role the API uses; ABAC permission check in every request handler; per-tenant AWS KMS customer managed key on report objects; separate connector secrets per tenant.
A3Break-glass workflow for any production data access with mandatory second approver + full audit; all prod data access logged to a separate account the prod role cannot write to; per-tenant customer managed keys limit blast radius of a single compromised operator session.
A4Deterministic, hermetic container builds; SBOM on every release; Sigstore signing; dependency pinning via pnpm-lock.yaml and requirements.txt with hashes; npm audit + pip-audit gate in CI; weekly Dependabot; release notes enumerate every upgraded dep.

3. Tenant isolation

Tenant isolation is the single most important property of the platform. We implement it at four layers.

3.1 Logical isolation. every row carries tenantId

Every tenant-scoped table in the primary data model has a non-null tenantId column, indexed, and is referenced by a foreign key to Tenant. This is visible in packages/db/prisma/schema.prisma: User, Role, Connector, CloudAccount, Resource, Relationship, Finding, CostDaily, Report, AuditEvent, and ApiToken all carry tenantId.

A query that does not filter on tenantId is a bug. We do not rely on application code alone to remember to filter. see 3.2.

3.2 Database isolation. Postgres Row-Level Security

The API connects to Postgres as a non-superuser role with FORCE ROW LEVEL SECURITY enabled on every tenant-scoped table. Each request starts a transaction, issues SET LOCAL app.tenant_id = '<tenantId>', runs the query, then commits or rolls back. Every RLS policy is USING (tenant_id = current_setting('app.tenant_id')::text).

This gives us three things:

  1. A forgotten where clause in application code cannot leak cross-tenant rows. the database refuses to return them.
  2. A buggy service that invokes a different service's DB role still cannot escape the scope. RLS is on the role used, not on the application identity.
  3. Migrations and operational tooling run as a separate role that bypasses RLS, and that role is only usable via the break-glass path documented in §7.

We run a CI job that asserts, for every model in schema.prisma marked tenant-scoped, that a matching RLS policy exists. A PR that adds a model without its policy fails CI.

3.3 Cryptographic isolation. per-tenant AWS KMS customer managed key

Every tenant has a dedicated AWS KMS customer managed key (alias/grandline/tenant/<tenantId>), created on tenant provisioning, with:

We do not use a single "platform" key for all tenants. A compromised app-level credential cannot Decrypt across tenants because the IAM policy attached to the runtime scopes AWS KMS usage to keys whose alias suffix matches the request's tenant.

Customers on the Enterprise plan can supply their own customer managed key (BYOK) with a grant that lets GrandLine Encrypt/Decrypt only. Revoking the grant makes their reports and stored secrets unreadable within minutes; we designed the platform to degrade gracefully in that case rather than fail open.

3.4 Network and storage isolation

Report objects live in a single S3 bucket under s3://grandline-reports/<tenantId>/… with a bucket policy that denies any GetObject/PutObject whose key prefix does not match the tag on the calling role. In practice, the runtime role has no s3: on the bucket at all. it has a scoped-down policy built at tenant-provisioning time that grants only <tenantId>/.

Every signed URL we hand back is scoped to a single object key, expires in 15 minutes, and is logged to the audit trail with the requesting user, the object key, and the expiry.


4. Cryptographic posture

Surface Algorithm / control
External TLSTLS 1.3 only; HSTS 2 years with preload; OCSP stapling; cert via ACM; certificate transparency monitored.
Internal service-to-service (SaaS)mTLS via a service mesh inside the cluster; workload identities are short-lived SPIFFE IDs.
At-rest (Postgres)AWS Aurora with customer managed key storage encryption; snapshots encrypted with the same key.
At-rest (Redis)Encrypted with a platform-level customer managed key; no tenant secrets are persisted in Redis. it is a BullMQ queue and a short-lived cache.
At-rest (S3 reports)SSE-KMS with per-tenant customer managed key; bucket key enabled.
At-rest (connector secrets)Envelope-encrypted with per-tenant customer managed key, stored as Connector.secretRef (opaque reference); the plaintext never touches Postgres.
At-rest (user passwords)argon2id, m=65536, t=3, p=4, per-user salt, 32-byte hash. We prefer WebAuthn and SSO; passwords exist for break-glass.
In transit (cloud connector calls)TLS 1.2+ using the cloud SDK defaults; AWS SigV4 / Microsoft Entra ID OAuth 2.0 bearer tokens (via MSAL) / GCP OAuth 2.0 bearer tokens obtained via Workload Identity Federation.
Signed URLs15-minute expiry, scope-limited, logged.
JWTsEdDSA (Ed25519); keys in AWS KMS; 10-minute access token, 24-hour refresh token bound to session + user-agent fingerprint.
RandomnessOS CSPRNG (/dev/urandom); no custom RNG.

We do not roll our own crypto. All primitives are from the cloud SDKs, libsodium, Node's crypto, or Python's cryptography.


5. Cloud access model

GrandLine reads cloud metadata. We do not write to customer clouds. The only requests we make are describe/list/get-style API calls.

5.1 AWS

Customers deploy a CloudFormation or Terraform module that creates a role, GrandLineReadOnly, with a trust policy that permits sts:AssumeRole from our SaaS control-plane account only if sts:ExternalId equals a per-connector secret we generate once and never display again. This defeats the classic confused-deputy problem. a customer who reused the same role ARN across vendors cannot be impersonated, because the ExternalId is unique to this connector.

The permission policy attached to the role is published verbatim at packages/connectors-aws/policy/grandline-readonly.json. It is a superset of SecurityAudit and ViewOnlyAccess. we explicitly did not use the managed policies because they both have gaps (e.g., SecurityAudit allows iam:GenerateCredentialReport, which writes) and we wanted a predictable surface. The policy grants only Describe, Get, List*, and a handful of ce: cost-explorer reads. We diff this policy on every release and call out changes in the release notes.

Sessions last 15 minutes. We refresh on demand. We do not cache long-lived AWS credentials anywhere; the assumed role's credentials live in the worker process memory and are discarded when the scan finishes.

5.2 Azure

Customers create a Microsoft Entra ID app registration in their tenant and grant it the Reader built-in role at subscription or management-group scope. We authenticate with Microsoft Entra ID Workload Identity Federation. a federated credential on the app registration. so our GitHub Actions and our production cluster both present OIDC tokens, and Microsoft Entra ID validates them against that federated credential. No client secret leaves the customer tenant. We rotate nothing; there is nothing to rotate.

5.3 GCP

Customers create a Google Cloud service account with roles/viewer, roles/iam.securityReviewer, and roles/bigquery.dataViewer at the organisation or project scope, then configure Workload Identity Federation with an external identity provider that trusts our control-plane cluster's OIDC issuer. Our workloads exchange their OIDC tokens at the WIF pool to impersonate the customer's service account with short-lived tokens (1 hour). Again, no long-lived key material crosses the boundary.

5.4 What we do NOT do

We do not read object contents (Amazon S3 objects, Azure Blob Storage contents, Cloud Storage (GCS) objects). We do not call IAM credential-issuing actions. We do not call any create/update/delete API. We log every API call we make, with the caller, the target, and the response size, to the audit trail.


6. Identity, authentication, and authorization

6.1 Authentication for humans

Supported factors:

Session tokens are 10-minute JWTs, bound to the originating IP /24 and user-agent hash. Refresh is a rotating token stored in a httpOnly, Secure, SameSite=Strict cookie. We revoke all sessions on password change, MFA change, or admin-initiated "sign everyone out".

6.2 Authentication for machines

ApiToken records store argon2-hashed tokens, a non-empty scopes array, and an optional expiresAt. Tokens can be scoped to a tenant only. they cannot cross tenants. A token is presented as Bearer <opaque> and every use writes to the audit trail.

6.3 Authorization. ABAC on top of RBAC

UserRole is the join table. Each row grants user → role within a (scopeType, scopeId) pair, where scopeType is one of tenant, account, or resource_tag. This means a user can be Viewer on the whole tenant, SecurityAdmin on a specific cloud account, and Owner on resources tagged team=platform, all at once.

Permission checks happen in a single middleware that takes the request's target tenant, cloud account, and/or resource, walks the user's role assignments, and returns the union of permissions. The middleware is ~100 lines and is covered by 400+ unit tests because it is the single most security-critical piece of code in the product.

System roles (isSystem = true) are shipped by GrandLine and cannot be edited by tenants. Custom roles are fine but must compose out of the permissions we ship.


7. Operator access and break-glass

GrandLine operators do not have standing access to customer data. The production database is in an AWS account that human engineers cannot read from directly. Access requires:

  1. An on-call or incident-response justification linked to a ticket.
  2. Approval from a second engineer (four-eyes) recorded in the same ticket.
  3. A time-boxed session (max 60 minutes) that produces a JIT IAM role.
  4. Full session recording. every SQL query is logged to a separate AWS account that the break-glass role cannot write to. We cannot delete our own tracks.

We run a quarterly tabletop where we assume an operator is malicious and walk through what they could achieve with 60 minutes of access. The output is a short report we share on request with Enterprise customers under NDA.


8. Audit and retention

AuditEvent is the append-only log. Every privileged action writes a row with tenantId, actorId, action (e.g., connector.create, report.download, user.role.assign), targetType, targetId, ip, ua, and a free-form details JSON.

On the Enterprise plan we mirror the log to S3 with Object Lock in Governance mode, a 7-year retention, and per-tenant customer managed keys. Customers can stream the log to their SIEM via:

We store a Merkle-chain hash in each row's details that links to the previous event's hash for that tenant, so a customer can detect tampering even without Object Lock.

Retention defaults:

Data Retention Deletion on tenant offboarding
Primary graph (Resource, Relationship)90 days of historyHard-delete in 30 days
Findings (closed)2 yearsHard-delete in 30 days
Cost data13 months (rolling)Hard-delete in 30 days
Reports2 yearsHard-delete in 30 days
Audit events7 years (Enterprise: customer choice)Customer managed key deletion makes them cryptographically unreadable; hard-delete is a customer-triggered job

9. Data residency

SaaS is offered in three regions: us-east-1 (N. Virginia), eu-west-1 (Ireland), and ap-southeast-2 (Sydney). Tenant data is pinned to the tenant's region. Postgres, S3, KMS, and workers all run in-region. We do not replicate tenant data across regions for disaster recovery; instead, cross-AZ multi-writer Aurora gives us DR within the region. Customers who need cross-region DR use the self-hosted edition.

Self-hosted installs run wherever the customer runs them. We do not phone home except for (a) optional license validation (Enterprise only, monthly) and (b) opt-in telemetry.


10. Supply chain

10.1 Build

All container images are built in a GitHub Actions hosted runner, in a dedicated release account, using a reproducible Dockerfile with pinned base images (digests, not tags). Build outputs are:

  1. A container image, tagged grandline/<component>:<semver>@<digest>.
  2. A CycloneDX SBOM (*.sbom.json) listing every runtime dependency with its version and hash.
  3. A Sigstore cosign signature over the image digest, produced with keyless OIDC (the GitHub Actions workflow is the signer identity).
  4. A SLSA v1.0 provenance attestation showing the source commit, builder, and inputs.

Customers pull our images from ghcr.io/grandline/* and can verify before running:

cosign verify ghcr.io/grandline/api:3.4.0 \
  --certificate-identity https://github.com/grandline/grandline/.github/workflows/release.yml@refs/tags/v3.4.0 \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com

10.2 Dependencies

Node deps are pinned via pnpm-lock.yaml; Python deps via requirements.txt with --hash entries. Every PR runs npm audit --audit-level=high, pip-audit, and trivy fs against the lockfiles. Findings block merge unless a CVE is annotated as not-applicable with a written reason in a dedicated file we publish quarterly.

We do not pull dependencies from public registries at runtime. All images are self-contained.

10.3 Secrets hygiene

No secrets in the repo. Pre-commit hooks run gitleaks. CI runs trufflehog against every PR. We rotate the customer-facing ExternalId on demand via a self-service flow.


11. Vulnerability disclosure and secure SDLC


12. Compliance mapping

GrandLine targets SOC 2 Type II, ISO 27001, and GDPR. The table below maps frequently-asked controls to the corresponding section of this paper.

Control family Reference
SOC 2 CC6.1. logical access§3, §6
SOC 2 CC6.6. encryption in transit§4
SOC 2 CC6.7. encryption at rest§4
SOC 2 CC7.2. detection of unauthorised access§7, §8
SOC 2 CC8.1. change management§10, §11
ISO 27001 A.5.23. cloud services§5
ISO 27001 A.8.3. information access restriction§3, §6
ISO 27001 A.8.24. cryptography§4
ISO 27001 A.8.28. secure coding§10, §11
GDPR Art. 32. security of processing§3, §4, §5, §7, §8
GDPR Art. 30. records of processing§8
GDPR Art. 17. right to erasure§8 (retention + hard-delete job)

SOC 2 Type II report and ISO 27001 certificate are provided to Enterprise customers under NDA. Readiness letters are available earlier in the sales cycle.


13. What is on the roadmap

To be honest about what is not yet done:


14. Questions

If anything in this paper is unclear, is inconsistent with what you observe in the product, or is missing a control you need, write to [email protected] with [security] in the subject line. We will answer in writing and update this document.

This document is reviewed and published quarterly. See the "Revision" field at the top for the current version.