CloudPanorama

Security

The self-assessment — every isolation and fail-closed invariant with the test that holds it — the scanners, the pen-test scope, and how to report a vulnerability.

This is the document a buyer's security team reads first, and the one the pen test is scoped from (platform task p5_7). It states what the platform promises, names the test that proves each promise, and says what happens when a scanner lights up. It is kept honest by being short: a line here without a test beside it is a claim, and claims are what a pen test is for.

The public summary, for the website, is written from this file and never the other way round.

1. The self-assessment

Every line is an invariant the code is built around (CLAUDE.md "Non-negotiable invariants", PROVISIONING_SPEC.md §3), the mechanism that holds it, and the test that fails when it stops holding. Status is held when the named tests passed in the latest evidence run, open otherwise (p7_28). The run is security-evidence.md beside this file, written by platform/tools/security-evidence.py: the date, the commit and the paths changed on top of it, a digest of the files it ran on, and every named test's result, with the checks that are not tests. CI runs the same tests when it runs, but pushes skip CI by the owner's decision of 2026-09-12, so the evidence run is what a status rests on. TestSecurityDocumentationDoesNotClaimUnverifiedGuarantees fails when a held line names a test that does not exist or did not pass in it, and when the digest is not this tree's. The digest covers every source file directly in a directory that holds a named test, the migrations, the two checks, the Dockerfiles, the modules' dependencies and the tool. So a change to any of them after the run leaves the run stale, and the run is repeated before the change lands. Severity is what a breach of the line would mean: high is another tenant's data or an unauthenticated write; medium is a single tenant's data reachable by the wrong person inside it; low is availability or hygiene.

# Invariant Mechanism Tests Severity Status
1 A tenant reads and writes only its own rows Row-level security on every tenant table, enforced on the panorama_app role; every tenant query runs inside db.InTenantTx, which sets app.tenant_id. tenants itself too, since migration 0158 (p7_48, S07): the app role sees its own tenant's row and no other db.TestInTenantTxCannotWriteIntoAnotherTenant, db.TestTheAppRoleSeesOnlyItsOwnTenantRow, db.TestTenantScopeDoesNotLeakToTheNextTransaction, db.TestSnapshotsAreTenantIsolated, db.TestNewPartitionsCarryRowSecurity, db.TestPerformanceIsTenantIsolated, db.TestPerformancePartitionsCarryRowSecurity, db.TestCostCrossTenantIsolation, db.TestCostIsolationCoversEveryCostTable, org.TestCrossTenantIsolation, db.TestCrossTenantIsolation (provisioning), db.TestCompositeFKsRefuseCrossTenantParents high held
2 Nothing reaches the database outside the tenant transaction CI greps for raw pool access outside internal/db (scripts/check-no-raw-pool.sh, make check-isolation) the guard itself (make check-isolation), run in the go job and in the evidence run high held
3 The BYPASSRLS role runs exactly the sanctioned operations db.InControlTx has a caller allowlist by file, each file named with the operation it performs, and a listed file that stops calling it is an error too (p7_48, S06: it was by package, so any new file in cliserve.go included — passed); partition DDL a second one (db, scheduler); both enforced by the same CI guard scripts/check-no-raw-pool.sh; db.TestMaintenanceFunctionsAreNotCallableByTheAppRole, db.TestOnlyTheOwnerCanSupplyTheClock high held
4 The tenant comes from the credential, never from the request Sessions, ingest tokens and agent tokens each resolve to one tenant; a handler is never reached without one; a public route leaves none behind middleware.TestTheTenantComesFromTheCredential, middleware.TestAHandlerIsNeverReachedWithoutATenant, middleware.TestAPublicRouteLeavesNoTenantBehind, handlers.TestOneTenantsTokenCannotReachAnothersRun, db.TestSessionCannotReferenceAUserFromAnotherTenant high held
5 A credential opens only the routes it is for The contract declares one scheme per route; the guard resolves that scheme and no other middleware.TestACredentialOnlyOpensTheRoutesItIsFor, middleware.TestARouteWithNoPolicyIsRefused, middleware.TestAPolicyMatchingNoRouteIsAlsoDrift high held
6 Auth fails closed No ingest credential and no explicit opt-out: the process exits. A database failure is a 503, never a 401 that lets a retry through cli.TestNoCredentialAndNoOptOutRefusesToRun, auth.TestOneSharedCredentialInAMultiTenantDeploymentIsAnError, middleware.TestADatabaseFailureIsNot401, middleware.TestARequestWithNoCredentialIs401 high held
7 Entitlement is not authorization, and neither is inferred Roles gate people; entitlements gate tenants; no row means nothing unlocked unless the deployment says ENTITLEMENTS_UNRESTRICTED, which serve refuses beside a licence middleware.TestARoleIsEnforcedWhereTheContractRequiresOne, handlers.TestNoRowMeansNoModules, config.TestUnrestrictedIsAnExplicitFlag, cli.TestServeRefusesTheFlagBesideALicense, entitlements.TestOneTenantsEntitlementIsNotAnothers medium held
8 A licence is verified before it is parsed, and cannot be rolled back ed25519 over the payload, checked first; issued_at decides newer, in the upsert's own WHERE license.TestATamperedLicenseIsRefusedLoudly, cli.TestAnOlderLicenseNeverReplacesARenewal, entitlements.TestAPreIssuedAtLicensedRowCannotBeRolledBack medium held
9 The public status page carries no identifier Its own response type, closed schemas, and a test that walks the body handlers.TestThePublicPageLeaksNothing, handlers.TestThePublicPageIsTenantScoped, handlers.TestThePublicPageIsRateLimitedPerAddress medium held
10 The platform never holds a customer cloud credential No field in the agent API can carry one; the agent runs on ambient credentials api.TestNoCredentialFieldsInAgentAPI (every property name in the agent API is checked against the credential rule from p4_1), agent.TestNoCredentialsUploaded, scrub.TestSecretScrubbed high held
10b The agent's platform token cannot be obtained by the code it runs Two roles of one image (D18, platform/docs/agent-execution-boundary.md): the broker holds the token and runs no customer code; the worker — another user, no token, non-dumpable, sweeping its process space before and after every attempt — runs OpenTofu; one workspace volume under a shared group, one channel per attempt. The worker's own environment is still built from nothing (envallow) agent.TestBoundaryTemplateCannotReadAgentTokenFromProc, agent.TestBoundaryTemplateCannotReadBrokerFiles, agent.TestBoundaryTaskCapabilityCannotCrossAccountOrAttempt, agent.TestBoundaryIsolatedRunnerCreateCancelDestroy (against the shipped image and users, make test-boundary, nightly and in the release gate — not per PR), worker and agent split tests, the agent acceptance test for the environment high held
11 An agent claims only its own tenant's and account's work The token names both; a claim is structurally scoped agentapi.TestClaimNeverCrossesAccountOrTenant, agentapi.TestAnotherAgentsTaskIsNotFound, drift.TestDriftNeverCrossesTenants, events.TestRemindersStayInTheirTenant high held
12 Every write is replayable without duplication Idempotency keys, run ids, claim ids; the replay matrix handlers.TestProtocolReplayMatrix, agentapi.TestClaimLostResponseIsIdempotent, ingest.TestReplayReturnsTheOriginalAndDoesNotBump medium held
13 Passwords and tokens are stored hashed; comparisons are constant-time; timing is uniform argon2id for passwords; hashed tokens; the timing suite auth.TestTheAcceptedShapesCostTheSameToRefuse, auth.TestAnEnormousStoredValueIsRefusedOnLength, make test-timing, in the evidence run medium held
14 Logins are throttled per address, per account as guessed from each address, and by an account-wide ceiling; an attempt made while locked is refused without being charged, so nobody can hold another person's account locked (p7_48, S01) auth.Throttle (AdmitAll), keyed by the client the deployment declares (CLIENT_ADDRESS_SOURCE), never by a header the caller chooses: five failures lock the (account, address) pair, twenty across addresses lock the account, a lockout restarts the count and doubles to a 15-minute cap auth throttle tests; TestALockedAccountCannotBeHeldLockedForever, TestASprayFromManyAddressesStillStops; config client-address validation low held
15 Every unauthenticated route has a per-address budget; every body-taking route has a size limit Derived from the contract by middleware.Policies (security: [] → 120/min, x-rate-limit where a route says otherwise — 60 on the public page, 300 on /auth/me; requestBody → 1 MiB unless x-max-body-bytes); applied by the guarded router before authentication and nowhere else; the shared decoders answer 413 when a capped stream runs out. The agent's register, claim and heartbeat ask for 1,200 a minute (p7_48, S09). A JSON body is checked against the contract's schema before its handler runs — maxLength, required, enums, patterns — 400 naming the field, 413 for an array over maxItems; the 192 MiB snapshot batch alone is not, its handler validating each record (p7_48, S02) middleware.TestUnauthenticatedRoutesAreRateLimited, middleware.TestEveryBodyRouteHasASizeLimit, middleware.TestEveryJSONBodyIsCheckedAgainstItsSchema, middleware.TestTheBodyIsCheckedBeforeTheHandler, handlers.TestADeclaredMaxLengthIsEnforced, middleware.TestARateLimitedRouteAnswers429PerAddress, middleware.TestABodyOverTheLimitIsRefusedBeforeTheHandler low held
16 Every response carries the security headers middleware.SecurityHeaders: a same-origin CSP with no inline allowance, nosniff, Referrer-Policy, a Permissions-Policy that turns everything off, COOP, and HSTS when COOKIE_SECURE middleware.TestTheHeadersAreOnEveryResponse, middleware.TestThePortalPolicyAllowsNoInlineAndNoForeignOrigin low held
17 CORS is an allowlist, never *; a 401 still carries it middleware.CORS(cfg.CORSOrigins), validated at startup middleware.TestOnlyAnAllowedOriginGetsCORSHeaders, middleware.TestAPreflightFromAnOriginNotOnTheListGetsNothing, middleware.TestA401StillCarriesCORSHeaders medium held
18 The image holds one static binary, the portal and nothing else; no secret is baked in Distroless, non-root; .dockerignore excludes every .env; the file list is checked in CI; the licence issuer never ships the image job's "Size, contents and licence key", which the evidence run repeats on a local build; license.TestTheIssuerNeverShipsInTheImage medium held
19 Sessions: a cookie that is HttpOnly, SameSite, Secure when told; a TTL that slides with use and ends twelve hours after the login however busy it is; logout deletes server-side auth.Sessions: SESSION_TTL (at most 12 h) is how long one lasts unused, and a refresh extends by it but never past auth.MaxSessionLifetime from the login (p7_48, S04: the line said "fixed" while every portal load could extend it for ever); COOKIE_SECURE handlers session tests (auth_session_integration_test.go), TestASessionEndsTwelveHoursAfterItsLogin medium held
22 A credential or privilege change ends the sessions that predate it, and a user is added, reset, re-roled and removed without SQL The user lifecycle (auth.Lifecycle, p7_20): a role change, a deactivation, a password change and a redeemed reset each end every session the user holds, in that order after the change commits; a reset is a 256-bit token stored hashed, redeemed once by its own UPDATE predicate, expiring after a day; the last enabled administrator cannot be demoted or deactivated (locked rows); another tenant's user is not found; every operation audits the email and never a password or token Rounds 1 and 2 (2026-09-07): the guarantee is the row, not the deletion — every password write increments users.password_version in the transaction that writes the hash, a login records on its session the version of the hash it verified (Repo.Credential, Sessions.Create), and the resolver refuses a session whose version is not the user's; no clock takes part, so neither a write whose transaction started before a login nor a login that read the hash before a reset can leave a usable session; the eager deletion is bounded tidying; migration 0150 removes the sessions the pre-0149 resolver already refused by timestamp, so the upgrade revives none (round 4) handlers.TestAdminOnboardsNonAdminWithoutSQL, handlers.TestPasswordResetIsExpiringSingleUseAndRevokesSessions, handlers.TestDeactivationAndRoleChangeInvalidateAccess, handlers.TestLastAdminAndCrossTenantUserMutationAreProtected, handlers.TestMultiUserPilotFlow, cli.TestCreateUserAndResetPasswordFromTheCommandLine, auth.TestAPasswordChangeEndsOldSessionsEvenWhenDeletionFails, auth.TestASessionIssuedDuringAPendingPasswordWriteIsRefused, auth.TestALoginInFlightAcrossAPasswordResetIsRefused, auth.TestUpgradeDoesNotReviveRevokedSessions medium held
23 An outbound notification reaches only a public address the tenant configured, over https, without following a redirect; the destination is never read back notify.ValidateDestination when a target is stored and again before each send (https on port 443 only, no credentials, every resolved address public — the template-source list, varschema.IsPrivateOrReserved, which since p7_48 (S08) also holds the unspecified, documentation, benchmarking and discard ranges and the IPv6 prefixes that embed an IPv4 address: NAT64, 6to4, Teredo), and the sender's own dialer applies the same check to the address it connects to; CheckRedirect returns the redirect unfollowed; a send failure is classified, never wrapped, so no error carries the URL; reads and audit rows carry host, hint and fingerprint only (p7_21) Round 1 (2026-09-07): the host a member reads is the parsed hostname (notify.HostOf), never a split on / — a query with no path before it had carried the credential; migration 0148 scrubs the audit rows that kept one notify.TestDestinationValidationRejectsUnsafeRedirectAndDNSPaths, varschema.TestTheRangesTheReviewNamedAreRefused, notify.TestASendFailureNeverNamesTheDestination, handlers.TestNotificationChannelLifecycleWithoutSQL, handlers.TestChannelMutationAndTestDeliveryRespectTenantTeamScope, handlers.TestChannelSecretsAreRedactedAndRotationWorks, handlers.TestAQueryOnlyDestinationNeverShowsItsQuery medium held
24 Object content is never read under the inventory role; a Cost & Usage Report is read under a second role the account scopes to that report's bucket and prefix cur.S3Stores assumes only the report's own role, with the tenant's external id, and refuses a report with none before any request; deploy/cloudformation/cost-report-role.yaml grants s3:ListBucket bound to the prefix and s3:GetObject on the prefix's objects, nothing else; the reader role keeps its no-object-read rule (p7_22) cur.TestCURAccessIsRestrictedToApprovedReportPrefix, cur.TestInventoryRoleStillCannotReadCustomerObjects, cur.TestCURConnectionReportsMissingPermissionAndIngestsFixture, onboarding.TestKnownContentBearingCallsAreNeverGranted medium held
20 An unsafe request on a session route, or on login/logout, comes from this deployment's own browser context; a body on such a route is JSON The browser boundary in the guarded router (middleware/csrf.go, p7_18), before the body is read and before the credential is resolved: Sec-Fetch-Site: same-origin/none, or same-site/cross-site only from an Origin in CORS_ORIGINS; without Fetch Metadata the Origin (then the Referer's origin) must be the server's host or listed, and null never is; a request with none of them is not a browser's. Bodies must be application/json. Bearer routes are exempt by credential kind. SameSite=Lax remains, and is not the boundary — it is about the site, and a same-site sibling carries the cookie middleware.TestUntrustedSiblingCannotMutateSessionAPI, middleware.TestSimpleTextPlainJSONCannotBypassCSRF, middleware.TestAllowedPortalAndBearerAgentRemainFunctional, handlers.TestLoginLogoutRefuseAnUntrustedOrigin; TestLoginLogoutCSRFPolicyInRealBrowser (web/e2e, a real Chromium against a same-site sibling, on demand) medium held
21 A request body is read within a budget, a response written within one, and a large route reads only so many bodies at once Per route, from the contract, set on the connection by the guarded router before the body is read (middleware/budget.go, p7_19): 30 s for a body unless x-body-timeout-seconds (120 s for the snapshot batch, 60 s for agent logs and template sources), 10 s to drain a bodiless route's unread body, 60 s past that for the response; the claim handler extends its own deadline to its poll, which PROVISIONING_CLAIM_POLL_TIMEOUT bounds at 4 minutes under the server's 5-minute ceilings; x-max-inflight caps concurrent bodies (2 batches, 16 log batches) with 429 and Retry-After; panorama_http_requests_inflight shows what is being read middleware.TestSlowBodyTimesOutWithinBudget, middleware.TestStalledUploadsCannotExhaustConfiguredConcurrency, middleware.TestEveryBodyRouteHasATimeBudget, handlers.TestLargeValidIngestIsNotRejectedByGlobalTimeout, handlers.TestLongPollClaimSurvivesReadDeadlinePolicy, cli.TestAPIServerCarriesTheCeilings, config.TestServerValidation low held
25 A user holds at most a set number of sessions at once auth.Sessions (p7_73): SESSION_MAX_PER_USER, 5 unless set and at most 100. The login past it ends that user's oldest sessions, sessions of an older password and expired ones first, in the transaction that creates the new one and under a per-user advisory lock, so logins that race at the cap cannot both find room; a login whose password changed after it verified it is refused and ends nothing (the login compares the account's current password version with the one it verified, and the store, under the lock, refuses a version older than a session the user holds); and every request ends a session that more of its user's newer sessions than the cap have overtaken, so a lowered cap, or the first release with one, reaches the sessions already open. During a rolling deploy, a replica still on the earlier release admits the sessions it already knows until it is replaced auth.TestALoginPastTheCapEndsTheOldestSession, auth.TestTheCapEndsAnExpiredSessionBeforeALiveOne, auth.TestConcurrentLoginsAtTheCapCannotExceedIt, auth.TestALoweredCapReachesSessionsAlreadyOpen, auth.TestAStaleLoginNeitherEvictsNorTakesAPlace, auth.TestAnOlderPasswordsSessionGoesBeforeALiveOne, auth.TestALoginInFlightAcrossAPasswordResetIsRefused, auth.TestALoginInFlightLeavesTheNewPasswordsSessionAlone, handlers.TestTheLoginPastTheCapEndsTheFirstSession, config.TestServerValidation medium held
26 A destructive administrator action needs the password again, on the session that takes it, within ten minutes The routes the contract marks x-step-up (p7_72): the guard derives them as it derives the role and, after the role check, refuses a session with no password check in auth.StepUpWindow (10 minutes) with 403 step_up_required; POST /auth/step-up checks the password under the login's per-account throttle and records it on that session alone; the mark on a bearer route, or one with no role, is a startup error; every administrator DELETE carries it, and so does every administrator update (PATCH or PUT) that can archive or switch off what exists, through a boolean archived, enabled, disabled or active in its body. The portal asks once however many refusals arrive together, and every waiting request settles on the answer middleware.TestEveryDestructiveRouteAsksForAStepUp, middleware.TestARouteThatArchivesOrDisablesAsksForAStepUp, middleware.TestPoliciesTheContractMustNotDeclare, auth.TestAStepUpIsRecordedOnItsOwnSession, auth.TestAStepUpNeedsTheRightPassword, handlers.TestADestructiveRouteWaitsForTheSessionsOwnStepUp medium held
27 A sign-in through a tenant's identity provider opens only that tenant, as the account the provider vouched for; where the tenant enforces it, a password opens nothing but a break-glass administrator's auth.SSO (p5_5): the OpenID Connect code flow with PKCE and a nonce. The attempt is kept in control.sso_attempts under its state's hash, taken once, for ten minutes, and names the tenant that started it. The ID token is verified against that tenant's registered issuer and client (signature, issuer, audience, expiry) and must carry the attempt's nonce. An address the provider says is unverified is refused, and one it does not vouch for either way is accepted only in the domains the tenant named; a first sign-in creates an account only in those domains when there are any. The account is the provider's subject: looked up by it first, bound once, and unique within the tenant and the issuer (0179), so a changed email or two first sign-ins at once make no second account; an address already bound to another subject is refused. A callback must carry the attempt cookie of the browser that started it. Every request of the flow (discovery, the token exchange, the keys) goes through a client that accepts only https on port 443 to hosts whose every address is public, checks the address it dials, and follows no redirect; an issuer that fails the policy is not saved. A successful sign-in refunds the throttle charges its own start and callback made, and nothing earlier. An account without a usable password confirms a step-up by signing in again at the provider (prompt=login, max_age=0), accepted only for the same session's user and within five minutes. Saving or removing the registration is audited in the same transaction, without the secret. A first sign-in's account is created with its identity in one insert, so a concurrent one under another email leaves no account behind; a new issuer is refused while accounts are bound to another until an administrator releases them, audited; migration 0179 refuses, and puts itself back, over the duplicates single sign-on's first version could leave, and 0180 takes older binaries out of service. A binding commits only under the tenant's current issuer (0181), which it reads under the registration's row lock, the lock the save takes before it counts: a sign-in racing an issuer change is refused, or is counted and refuses the save. The session is issued by the password path's own Sessions.Create; the return path is a path on this site; the client secret is never returned. Enforced mode answers a correct password with 403 sso_required, and a wrong one as any wrong password auth.TestASignInThroughTheProviderIssuesASession, auth.TestAnAttemptIsTakenOnceAndExpires, auth.TestTheIDTokenMustSayWhatTheAttemptNeeds, auth.TestTheAccountIsItsSubjectsAndItsTenants, auth.TestEnforcedSignOnKeepsPasswordsForBreakGlassOnly, auth.TestTheReturnPathStaysOnTheSite, handlers.TestASignInThroughTheProviderEndsInASessionCookie, handlers.TestAFailedSignInGoesBackToTheLoginPage, handlers.TestTheReturnPathStaysOnThisSite, handlers.TestEnforcedSignOnAnswersAPasswordWithSSORequired, handlers.TestTheRegistrationNeedsTheCapabilityAndKeepsItsSecret, auth.TestTheSignInNeverDialsAPrivateAddress, auth.TestAnIssuerOutsideThePublicInternetIsRefused, auth.TestTheOutboundPolicyRefusesWhatIsNotPublicHTTPS, auth.TestTheClientChecksTheAddressItDials, auth.TestTheClientRefusesAPrivateEndpointTheDocumentNames, auth.TestTheClientFollowsNoRedirect, auth.TestSuccessfulSignInsLeaveOnlyTheEarlierFailure, auth.TestAStableSubjectWhoseEmailChangesIsOneAccount, auth.TestConcurrentFirstSignInsMakeOneAccount, handlers.TestAStateFromOneBrowserIsRefusedInAnother, handlers.TestAnAdministratorWhoOnlySignsOnStepsUpAtTheProvider, handlers.TestAProviderChangeIsAuditedWithoutItsSecret, auth.TestFirstSignInsOfOneSubjectUnderTwoEmailsMakeOneAccount, auth.TestAnIssuerChangesOnlyOnceItsAccountsAreReleased, auth.TestDuplicateIdentitiesRefuseTheUpgradeUntilReleased, auth.TestAFailedIdentityBuildIsPutBackBeforeTheNextRun, auth.TestBinariesBeforeTheIdentityFloorStopServing, handlers.TestAnIssuerChangeIsRefusedUntilTheAccountsAreReleased, auth.TestAnOldProviderSignInCannotBindUnderANewIssuer, auth.TestAnOldProviderSignInCannotBindAnExistingAccountUnderANewIssuer high held
28 The Admin area's reads are an administrator's alone, and the audit log a tenant reads is its own GET /admin/audit and GET /admin/overview (p7_88) carry x-role: admin, which the guarded router derives from the contract and enforces before the handler runs. The audit log is read in the tenant's transaction, under audit_log's forced row-level security (0128) and with the tenant in the query besides, newest first by id and a page at a time by an id cursor; the overview counts the users in the same kind of transaction and takes the licence as GET /entitlements answers it. The portal's Admin area refuses a member by name and asks the API for nothing handlers.TestTheAuditLogIsTheTenantsOwnNewestFirst, handlers.TestTheAuditLogIsAnAdministratorsAlone, handlers.TestTheAdminOverviewIsOneRead, middleware.TestThePolicyDerivedFromTheContract high held
29 A Google Cloud project is read with metadata-only permissions, as the platform's own identity, and a module of one cloud never runs against an account of the other The reader's custom role (deploy/gcp/reader-role.yaml, p7_82) grants exactly the Google collectors' permissions and verification's, and none of the content-bearing ones its test names (table rows, routine bodies, a function's environment, Datastore documents, keys, secrets, logs). The platform holds no key file: it exchanges its own AWS credentials at Google's Security Token Service and impersonates the project's reader. Its token carries the cloud-platform scope, the simplest single scope every method the reader calls accepts (IAM also lists its own, and Cloud Monitoring does not accept the read-only one); a scope can only narrow a token, and what the reader can do at all is the custom role's. The scheduler pairs each module with accounts of its cloud, and the worker picks credentials by the module's cloud and refuses a job whose module and account are different clouds before it makes any onboarding.TestTheGoogleReaderRoleIsAnExactAllowlist, onboarding.TestKnownContentBearingGooglePermissionsAreNeverGranted, gcpclient.TestAReaderSignsInThroughThePlatformsAWSIdentity, gcpclient.TestTheScopeIsOneEveryMethodAccepts, worker.TestAModuleOfTheOtherCloudIsRefused, scheduler.TestAModuleRunsOnlyForAccountsOfItsCloud high held
30 A person reaches only their own sessions, inbox and settings Every route behind a person's own settings (p7_89) takes the account from the session and nothing from a body or a path: /auth/me, /auth/me/notifications, /auth/me/teams, /auth/sessions and /auth/inbox. A session is named by an id derived from its token's hash, never stored and never a token; ending one is scoped to the tenant and the account, a session that is not the person's is not found, and the session making the request cannot be ended through it. The inbox is read and marked in the tenant's transaction under row-level security, with the account in the query; an id that is not the person's is passed over. What the notifier writes to it is only what its person may see: an administrator every account and team, a member their teams and their teams' accounts, an account no team owns the administrators alone notify.TestAnInboxHoldsOnlyWhatItsPersonMaySee, handlers.TestAPersonSeesTheirOwnSessionsAndEndsTheOthers, handlers.TestNobodyReachesAnotherPersonsSessions, handlers.TestAPersonsInboxAndChoicesAreTheirOwn, notify.TestReadingAndMarkingTouchOnlyYourOwn, middleware.TestThePolicyDerivedFromTheContract high held

No open highs as of the evidence run of 2026-09-17 (line 10b was open for the day between the decision and its implementation, p7_2, on 2026-09-06). Two lines are honest about being narrower than their name: line 13's uniform timing is measured for the login path and the token parsers, not for every comparison in the codebase; line 15's budget is per address, so a distributed flood is bounded per source, not in total — that is the load balancer's job, not this process's. Line 21 bounds each request and each large route, not the process's total connections: that ceiling is the edge's too, and deploy/proxy/nginx.conf.example shows where it goes.

What this does not claim

2. The external pen test

To be booked before the first pilot, by the owner (p5_7): the vendor, the dates and the authority to test are the owner's. It is scoped to what a buyer's security team asks about and nothing generic. The scope, the test environment and the triage are in platform/docs/security-assessment.md (p7_28):

  1. Cross-tenant isolation under RLS. Two tenants, credentials for both, every read and write route; the tester's goal is any row of the other tenant through the API, through a crafted credential, or through an ingest or agent token. Lines 1–5 and 11 above are the claims under test.
  2. The agent protocol. A hostile agent with a valid token: claim another account's work, replay to duplicate an apply, exfiltrate a credential through a log line, reach the platform token from inside OpenTofu. Lines 10–12 are the claims under test.
  3. The cloud identities. The reader role, the cost-report role and the platform's own identity each grant only what their lines say, and the reader role reads no object content. Lines 10 and 24 are the claims under test.
  4. The browser. The session cookie, the CSRF boundary from a hostile same-site page, the headers and the content-security policy. Lines 16, 19, 20 and 22 are the claims under test.

Findings are triaged by the rule in §4 and land in this table as new lines with their tests.

3. Scanners

Three run in CI, in the job that builds the thing they scan. Pushes skip CI by the owner's decision of 2026-09-12; the scanners run again when CI does, and the release gate (p7_31) requires them:

Scanner Where What it fails on
govulncheck the go and agent jobs, after the tests any vulnerability in a function the binary actually calls (call-graph aware; an unreached vulnerable function is reported, not failed)
npm audit --omit=dev --audit-level=high the web job, after the build a high or critical in a dependency that ships in the built portal; the full audit including dev tooling is printed and does not fail
Trivy CLI 0.74.0, a pinned release verified by checksum, two passes the image job, after the image is built and before it is pushed the report pass prints every finding at every severity, fixed or not, and never fails; the gate pass fails on a critical with a fix available, in an OS package (distroless/base-nossl's glibc, tzdata and their neighbours, since p7_34) or in either Go binary's module list — panorama and the signing helper — and prints nothing else

4. The triage rule

A scanner finding is handled by severity and by whether it is reached, not by whether CI is red:

  1. Critical or high, reached, fix available: the build is red and stays red; the dependency is bumped in a commit of its own, the same day. No suppression.
  2. Critical or high, reached, no fix: the build is red; the finding is suppressed with an expiry (govulncheck: not supported — the module is replaced or the call removed; npm: an overrides entry with a comment; Trivy: a .trivyignore line with the date and the reason) and a line in this file under "Accepted findings" with the mitigation. Reviewed at the next release.
  3. Not reached (govulncheck says so; or the package is dev-only for npm): recorded, not suppressed, bumped with the next routine update.
  4. Medium and below: bumped with the next routine update; never suppressed, because a suppression that outlives its reason is how the list stops being read.

A suppression without an expiry date or without a line in this file is a lint failure of the reviewer, not of the tool.

Accepted findings

Recorded 2026-09-04, from the first run: five advisories in modules the binary requires but did not call (govulncheck, call-graph aware; rule 3), whose fixed versions needed the Go 1.26 line. Resolved 2026-09-11 by the Go 1.26 move (p7_59): x/crypto v0.57.0 and kin-openapi v0.149.0 carry the fixes. The move was brought forward because Trivy's gate reads module versions, not call graphs, and failed the image on kin-openapi's critical GHSA-r277-6w6q-xmqw — which rule 3's "recorded, not suppressed" had left standing. One remains, with no fix published:

Advisory Module Fixed in Why accepted
GO-2026-5932 golang.org/x/crypto v0.57.0 no fix published not reached (openpgp)

Resolved 2026-09-11: GO-2026-6355 and GO-2026-6354 (x/crypto, fixed in v0.56.0), GO-2026-6112 and GO-2026-6095 (kin-openapi, fixed in v0.144.0). The same day the agent module's golang.org/x/text v0.25.0, reached through the HCL parser (GO-2026-5970, fixed in v0.39.0), went to v0.42.0 under rule 1.

Review: at the next release.

Recorded 2026-09-11 (p7_34), from the first scan of the image carrying AWS's IAM Roles Anywhere signing helper. aws_signing_helper 1.8.5 is AWS's own release binary, the latest, pinned by the checksum AWS publishes; its modules are AWS's to bump, and it moves when AWS releases the next one. Trivy reads its module list; govulncheck in binary mode finds the SSH and OpenPGP symbols linked in, and in source mode, run on the helper's v1.8.5 tag, finds none of them called — the helper's own code imports only pbkdf2, pkcs12 and scrypt from x/crypto, and makes no SSH connection and serves none. Rule 3 applies.

Advisory Module (in the helper) Fixed in Why accepted
GO-2026-6303 (CVE-2026-56854, high) golang.org/x/crypto v0.53.0 v0.55.0 not called: ssh.NewServerConn; the helper serves no SSH
GO-2026-6355 golang.org/x/crypto v0.53.0 v0.56.0 not called: ssh.Dial and friends
GO-2026-6354 golang.org/x/crypto v0.53.0 v0.56.0 not called: as above
GO-2026-5932 golang.org/x/crypto v0.53.0 no fix published not called: openpgp

Review: at the next helper release, or at the next platform release, whichever is first.

5. Reporting a vulnerability

Write to contact@cloudpanorama.com with "security" in the subject. Reports are acknowledged within two working days; a fix for a confirmed high ships before any other work. Please do not open a public issue for a security finding.