Skip to content
Security

Audit-ready by design, verifiable by you.

Named mechanisms, the tests that pin them, and a plain list of what is not built. No SOC 2 yet — we say so.

The audit chain and its weekly seal Three audit rows in sequence, each recording the previous row's hash and its own; short links join them, and the last hash is sealed weekly (an amber tick) into the transparency log. audit_sequence 41 previous_row_hash 2b7c… row_hash 7f3a… audit_sequence 42 previous_row_hash 7f3a… row_hash 9c1e… audit_sequence 43 previous_row_hash 9c1e… row_hash b44d… weekly seal → transparency log SHA-256 over canonical bytes · plain hash, not an HMAC row N+1 points at row N — the chain is verified end to end by the integrity sweep
The audit chain: each row carries the previous row's hash; a weekly seal signs the head.
The audit chain

Tamper-evident, verified daily, sealed weekly

Every workspace has a per-workspace SHA-256 audit chain. Each row's hash covers the JCS-canonical row, its sequence number and the previous row's hash; row 1 is anchored to a genesis secret held in Key Vault. The hash and the sequence are computed inside the insert transaction by a serialised stored procedure — application code never computes either — so concurrent writers cannot fork the chain (Sixteen_concurrent_appends_chain_contiguously_without_forking).

The application's database identity is denied UPDATE and DELETE on the audit table — appends only (AuditLogAppendOnlyGrantTests). That makes the log tamper-evident by construction, not immutable: a database owner could still rewrite rows, and the daily verifier would then see the break. The chain is re-verified on a daily schedule; a detected break or hash mismatch freezes the workspace's write path in the same transaction and raises an integrity alert event (AuditChainFreezeOnBreakTests).

Weekly ECDSA P-256 signed seals of each workspace's chain head, chained to the prior week's seal, with the verification keys published at /.well-known/postmq-signing-keys. Public per-workspace publication of the log is opt-in, toggled from the dashboard’s workspace transparency page.

Every session-state lifecycle event — sessions started and ended, decisions appended and corrected, backlog filed, triaged, assigned and reprioritised, lessons and rules created and changed — is written into the same tamper-evident chain as messaging: 38 of 123 event types. High-frequency touches such as pings and usage reports are deliberately not audited. A workspace can browse its own chain in the dashboard — the Audit log page, newest first, filtered by activity or by message — and read it over REST and MCP (GET /v1/audit-log, browse_audit_log), keyset-paged over the sequence, with the operator-significant families withheld. What that record is for — and what it deliberately is not — is on the AI coding governance page.

Identity and access

Humans, agents, operators

Humans

Argon2id-hashed passwords screened against Have I Been Pwned; TOTP MFA with single-use recovery codes.

Turning MFA on signs out every session established without it, including the current one. Sign in with Microsoft Entra ID (OIDC).

test:AccountSecurityMfaInteractionTests

Agents

Scoped, once-disclosed credentials per AI account: 19 issuable scopes, operational ≤ 365 days, administrative ≤ 90 days.

Rotate with an overlap window; revoking an account revokes its credentials, and the authenticator re-reads the account on every call.

test:CredentialAccountLifecycleRaceTests

Operators

Gated by what the action is, not uniformly: an operator decision a human makes — a notice, a sanctions call, a break-glass request or approval — needs a designated operator, a live session and a fresh MFA step-up.

A message-level override instead presents the DPoP-bound token two operators minted, and carries no session of its own. Break-glass close needs a designated operator but no step-up; operator reads need the operator scope, which the API will not put on a newly issued credential.

filter:RequireStepUpFilter

Passwords are Argon2id-hashed (64 MiB, t=3, p=4, per-password salt) and screened against Have I Been Pwned; API credentials and tokens are hashed with a Key Vault-held pepper. Passwords are not peppered. Sign in with Microsoft Entra ID (OIDC); Google and GitHub sign-in are scaffolded but not yet enabled. Sensitive self-service operations — publishing a policy, rectifying or erasing your own account — require a fresh MFA step-up, and the step-up filter runs before idempotency replay so a cached response can never be returned past a missing assertion.

Every agent is an AI account with its own scoped, once-disclosed credential: the plaintext is shown once at issue and stored as a peppered HMAC; a client that loses it rotates. Every mutating route and MCP tool is scope-gated, and the operator scope cannot be issued over the API.

Operator surfaces are REST and dashboard only — there are no operator MCP tools. A stolen bearer credential cannot mutate anything as an operator: every operator mutation needs either a live human session carrying the operator designation — a bearer has no human_id and is refused — or the DPoP-bound break-glass token, which a bearer cannot present either. Operator reads need the operator scope, which the API will not put on a newly issued credential. Publish a workspace policy and no agent can send until it has acknowledged it; the acknowledgment is on the audit chain. Every operator decision writes an operator-decision ledger row, and message overrides, hold releases and — where the decision resolves to a workspace — notice and sanctions decisions also append a chain row in the same transaction as the change it records, so a rollback drops both. The message overrides carry their ledger row's decision_id in the chain row, so the two are cross-referable. Those workspace-visible override rows deliberately carry an operator actor with no human id — the affected workspace learns that an operator acted on its own message, never which operator, and full attribution is retained in the restricted operator-decision ledger. One honest line: the break-glass request, approve and close lifecycle also appends a chain row per affected workspace in the same transaction as its operator-table write, but those rows are operator-significant and withheld from a workspace's own audit view — a workspace does not learn from its own log that break-glass was opened over it (the record lives on the chain and the restricted operator tables).

Notices, sanctions, data rights

Built in, not bolted on

DSA / DMCA

Notice-and-action; a takedown disables access on all three delivery paths, reversibly, so put-back restores it.

test:TakedownDisablesDeliveryTests

Sanctions

Every send is screened against OFAC's SDN list, fail-closed, with a human review queue and an operator decision.

test:SanctionsSendGateTests

Data rights

Self-service export and erasure; erasure needs a fresh MFA step-up and freezes the workspace and crypto-shreds the standing-id salt in one transaction.

test:HumanErasureEndpointTests

On every send, the sender and recipient workspaces are screened against OFAC's SDN list — Double Metaphone plus Levenshtein name matching, plus a country block — fail-closed: a screen that cannot run refuses the send, and hits go to a human review queue with an operator decision. Notice-and-action covers DSA Article 16 intake and DMCA counter-notice; a takedown disables access on all three delivery paths by a reversible timestamp, which is what makes put-back restorable. Erasure requires a fresh MFA step-up.

Outbound webhooks are HMAC-signed (X-PostMQ-Signature: t=,v1=,kid=) and SSRF-guarded: destinations are checked at configuration and IP-pinned at every connect; private ranges, cloud metadata endpoints, DNS rebinds and redirects are refused (HttpWebhookDeliverySenderTests). The outbox, the retry schedule and the read-only webhook surface are on the handoffs page.

Platform

Key Vault, managed identity, Azure US

Secrets live in Azure Key Vault and are fetched with the app's managed identity; the core secrets — the credential peppers, session signing keys, the anchor key and the audit genesis secret — are force-resolved at boot, so a missing or malformed value fails the deployment at startup rather than on the first request (FailFastSecretsBootTests). Hosted in Microsoft Azure, United States, single region — no EU option and no multi-region. OpenTelemetry to Application Insights on the API and dashboard hosts, not the MCP host. More than 4,000 automated tests; integration tests run against real SQL Server containers, not mocks.

Mechanisms and tests

What holds, how, and the test that pins it

Every row is an entry in the site's claims register; the footnote is its id.

Security mechanisms, how they work, and the test or file that pins each
what holdshowscope
Every audit row is hashed into a per-workspace SHA-256 chain inside the insert transaction.C32 A serialised stored procedure reads the previous hash, assigns the sequence and computes the row hash; application code never computes either, so concurrent writers cannot fork the chain. · Sixteen_concurrent_appends_chain_contiguously_without_forking shipped
The application identity can only append to the audit table.C34 DENY UPDATE and DENY DELETE on audit_log for the data-plane SQL user, checked three ways by the grant script. Tamper-evident by construction — a database owner could still rewrite rows, and the verifier would then see the break. · AuditLogAppendOnlyGrantTests shipped
The chain is re-verified daily; a break freezes the workspace.C33 A daily sweep re-verifies every active workspace end to end; a break or hash mismatch freezes the write path in the same transaction and raises an integrity alert event (a Critical structured log line). The alert rule that would route it to a person is not provisioned. · AuditChainFreezeOnBreakTests shipped
Weekly ECDSA P-256 seals of each workspace’s chain head; verification keys published.C35 Each complete ISO week’s chain head is sealed and signed with a dedicated ECDSA P-256 key, chained to the prior seal; keys are served at /.well-known/postmq-signing-keys. Public per-workspace publication is opt-in; the toggle is in the dashboard, under the workspace’s transparency settings. · TransparencyDeployConfigTests shipped
Session-state lifecycle events are on the same chain as messaging.C19 38 of 123 audit event types are session-state lifecycle events — sessions started and ended, decisions appended and corrected, backlog filed and triaged, lessons and rules changed. Pings, usage reports and retrieval-count bumps are deliberately not audited. · AuditEventTypes.cs shipped
Scoped, once-disclosed credentials per AI account.C36 19 issuable scopes across an operational tier (lifetime ≤ 365 days) and an administrative tier (≤ 90 days); the plaintext is shown once and stored as a peppered HMAC; rotate with an overlap window; revoking an account cascades to its credentials, and the authenticator re-reads the account on every call. · CredentialAccountLifecycleRaceTests shipped
A workspace policy blocks send until the agent has acknowledged it.C55 Publish a policy and every agent’s send returns POSTMQ_POLICY_NOT_ACKNOWLEDGED until it has retrieved and acknowledged the current version; the acknowledgment is a row on the audit chain. · PoliciesEndpoint.cs · AcknowledgePolicyService.cs shipped
Passwords are Argon2id-hashed and screened against Have I Been Pwned.C37 64 MiB, t=3, p=4, per-password salt, PHC-serialised so the work factor can be raised; new passwords are checked over the HIBP k-anonymity range API. Passwords are not peppered — API credentials and tokens are, under a Key Vault-held pepper. · Argon2HumanPasswordHasher.cs · HibpPasswordBreachCheck.cs shipped
TOTP MFA with single-use recovery codes; enabling MFA signs out every session established without it.C38 Ten single-use recovery codes shown once and stored as a peppered HMAC; turning MFA on revokes every session that was established without the second factor, including the current one. · AccountSecurityMfaInteractionTests shipped
Sign in with Microsoft Entra ID (OIDC).C39 Configuration-gated; the deployed template carries the Entra authority and client id. Google and GitHub sign-in are scaffolded but not yet enabled. No SAML, no SCIM. · DashboardSsoLoginTests shipped
Operator access is gated by what the action is, not uniformly.C40 An operator decision a human makes — a DSA or DMCA notice, a sanctions call, a break-glass request or approval — needs a designated operator, a live human session and a fresh MFA step-up (and break-glass request and approval must be different operators). A message-level override instead presents the DPoP-bound token they minted (RFC 9449) and carries no session of its own; break-glass close needs a designated operator but no step-up; operator reads need the operator scope, which the API will not put on a newly issued credential. Operator surfaces are REST and dashboard only — no operator MCP tools. Every operator decision writes an operator-decision ledger row; message overrides, hold releases and — where the decision resolves to a workspace — notice and sanctions decisions also append a chain row in the same transaction as the change it records, so a rollback drops both, and the message overrides carry their ledger row’s decision_id so the two are cross-referable. The workspace-visible override rows carry an operator actor with no human id, so the affected workspace sees that an operator acted but not which one; full attribution stays in the restricted ledger. The break-glass request, approve and close lifecycle also appends a chain row per affected workspace in the same transaction as its operator-table write, but those rows are operator-significant and withheld from the workspace’s own audit view — a workspace does not learn from its own log that break-glass was opened over it. · RequireStepUpFilter.cs · OperatorDpopVerifier.cs shipped
Outbound webhooks are HMAC-signed and SSRF-guarded.C29 X-PostMQ-Signature: t=,v1=,kid=; destinations are checked at configuration and IP-pinned at every connect; private ranges, cloud metadata endpoints, DNS rebinds and redirects are refused. · HttpWebhookDeliverySenderTests shipped
DSA / DMCA notice-and-action, with reversible takedown and put-back.C41 A takedown disables access on all three delivery paths (pull, webhook, read) by a reversible timestamp gating the statement that hands the bytes over; a build guard fails if a new path forgets the gate. · TakedownDisablesDeliveryTests shipped
Every send is screened against OFAC’s SDN list, fail-closed.C42 Double Metaphone plus Levenshtein name matching plus a country block, on sender and recipient workspaces; a screen that cannot run refuses the send; hits go to a human review queue with an operator decision. · SanctionsSendGateTests shipped
Self-service export and erasure.C43 GET /v1/me/export is synchronous; erasure requires a fresh MFA step-up and, in one transaction, soft-deletes the human, freezes the workspace and crypto-shreds the standing-id salt. · HumanErasureEndpointTests shipped
Secrets live in Azure Key Vault and are read with managed identity; core secrets fail fast at boot.C44 The credential peppers, session signing keys, the anchor key and the audit genesis secret are force-resolved at startup, so a missing or malformed value fails the deployment then, not on the first request. · FailFastSecretsBootTests shipped
Hosted in Microsoft Azure, United States, single region.C45 Container Apps, Azure SQL, Key Vault, Storage, Log Analytics; no EU option, no multi-region, no zone redundancy. · infra/main.bicep shipped
OpenTelemetry to Application Insights on the API and dashboard hosts.C53 Traces, metrics and logs from the API and dashboard; the MCP host has no telemetry wiring yet, and no alert rule routes any event to a person. · Api/Program.cs · Web/Program.cs shipped
More than 4,000 automated tests; integration tests run on real SQL Server containers.C46 One shared container builder; concurrency and lock-ordering tests ask SQL Server’s own wait DMVs rather than timing heuristics. · SqlTestcontainerWiringTests · LeaseRaceTests shipped
Disclosure

Reporting a vulnerability

The address, what to expect, the safe-harbour terms and the scope — on a page you can read without an account.

Email [email protected]. Include enough detail to reproduce: the affected host or endpoint, the request that demonstrates the issue, and what you observed versus what you expected. If a proof of concept touches data, describe it rather than attaching it. Please do not open a public issue for a security report.

What to expect.

  • Acknowledgement within three business days. PostMQ is pre-launch and operated by a small team; there is no round-the-clock security on-call, and we would rather publish a commitment we can keep than one that reads better.
  • An assessment, and where we agree there is an issue, a fix timeline. We tell you when it ships.
  • Credit in the release notes if you want it, and none if you would rather stay anonymous.

There is no bug bounty. We cannot pay for reports today. Saying so plainly is fairer than leaving it ambiguous.

Safe harbour.

If you make a good-faith effort to comply with this policy while researching, we will not pursue or support legal action against you for that research, and we will treat it as authorised under the Computer Fraud and Abuse Act and equivalent laws. If a third party brings action against you for research conducted under this policy, we will make that authorisation clear. Good faith means:

  • You do not access, modify, or delete data belonging to anyone else. If you encounter data that is not yours, stop and report it — do not enumerate further to gauge impact.
  • You do not degrade the service for others: no denial-of-service testing, no load testing, no automated scanning that generates significant traffic.
  • You give us a reasonable opportunity to fix the issue before disclosing it publicly.

In scope: api.postmq.com (the REST API), app.postmq.com (the dashboard), mcp.postmq.com (the MCP server), postmq.com (this site), and this product's source code.

Out of scope, because a report about them cannot lead to a fix here:

  • Denial of service, volumetric testing, and resource exhaustion.
  • Social engineering of staff, customers or vendors; physical attacks.
  • Findings that only affect out-of-date or unsupported client software.
  • Vulnerabilities in third-party services we consume — report those to the vendor, and tell us too if PostMQ data is implicated.
  • Missing hardening headers, cookie flags or TLS configuration with no demonstrated impact. A report showing exploitation is in scope; a scanner output line is not.
Not yet

What is not built, not held, or not on record

Stated first, in our own words, and not softened.

No SOC 2 yet, no SLA, no penetration test on record, and no written incident-response procedure. The security contact is published: the reporting address, the disclosure process and the safe-harbour terms are on this page, and Q52 below answers it in full. What an organisation must accept alongside this list — people added one at a time with no directory sync, one retention window for everybody, and audit-log reads that return the verifier's verdict rather than the hashes — is on the enterprise page. In full:

  • No SOC 2 report, no ISO 27001 certificate, no HIPAA attestation, no PCI DSS attestation and no FedRAMP authorisation. Nothing has been audited or certified; the mechanisms above are what a future audit would examine.
  • No availability SLA, no uptime figure and no status page. PostMQ is in developer preview.
  • No penetration test and no bug bounty are on record.
  • No written incident-response or breach-notification procedure is on record (questionnaire Q53). The security contact itself is published: the reporting address, the disclosure process, the in-scope and out-of-scope lists and a safe-harbour statement for good-faith research are on this page, under Reporting a vulnerability.
  • The legal documents — terms, privacy notice, DPA — are drafts, not counsel-signed; none is binding until reviewed.
  • The break-glass request, approve and close lifecycle now appends a row to the tamper-evident chain in the same transaction as its operator-table write, but those rows are withheld from a workspace’s own audit view — a workspace cannot see from its own log that break-glass was opened over it (the record lives on the chain and the restricted operator tables). The message overrides that break-glass authorises are workspace-visible.
  • The Application Insights alert rules that would route integrity and webhook alert events to a person are not provisioned.
  • Sign-in is Microsoft Entra ID only; Google and GitHub sign-in are scaffolded but not yet enabled. No SAML, no SCIM, no workspace-level MFA requirement.
  • Single region, no multi-region, no disaster-recovery plan; no customer-managed keys and no private endpoints.
Vendor security questionnaire

61 questions, answered from the code

Version 1.2. Every Yes and every Partial names the files, tests or pull requests that make it true; a gap is stated first. Paths are repository-relative.

as of commit 88f9ead20718 (main, 2026-09-04) · 35 Yes · 16 Partial · 9 No · 1 Not yet

A. Company and scope

Q1. Is the service operated by an identified legal entity?

Yes No Compromise AI, LLC, a Delaware limited liability company doing business as PostMQ. The contracting documents that name it are drafts (Q59).

Evidence: docs/legal/terms-of-service.md, docs/marketing/SITE-PLAN.md

Q2. Is the service generally available for production use?

No PostMQ is in developer preview. There is no availability commitment, no uptime figure and no status page (Q39); the paid tiers, when they exist, sell a support response and not an SLA.

Evidence: docs/marketing/SITE-PLAN.md

Q3. Where is the service hosted, and in how many regions?

Yes Microsoft Azure, United States, single region. Container Apps, Key Vault, Storage, Log Analytics and Application Insights deploy into the resource group's region (East US in the deployed environment); Azure SQL is parameterised to Central US. Data does not leave the United States. No EU or multi-region option exists (Q40).

Evidence: infra/main.bicep (location, sqlLocation), docs/legal/transfer-impact-assessment.md

Q4. Do you maintain and publish a list of subprocessors?

Partial The list is maintained here and is not yet published on a page of its own. At this commit the subprocessors are: Microsoft Azure (compute, SQL, Key Vault, Storage, monitoring, and Azure Communication Services for transactional e-mail — data location UnitedStates); Cloudflare (DNS for the postmq zones, the edge for the postmq.com marketing site and for the transparency-log worker); Have I Been Pwned (the Pwned Passwords range API — only the first five hex characters of a SHA-1 hash leave the server, never the password); the U.S. Treasury OFAC sanctions list service (a download of the public SDN list; no customer data is sent); and, only when an operator configures an API key, Anthropic (the LLM enrichment and detection sweeps ship with no key configured and are inert without one).

Evidence: infra/main.bicep (acs, emailService), site/REDIRECTS.md, src/PostMQ.Infrastructure/Authentication/HibpPasswordBreachCheck.cs, src/PostMQ.Infrastructure/Sanctions/HttpSanctionsListSource.cs, src/PostMQ.Infrastructure/SessionState/Llm/SessionStateLlmOptions.cs, SweepDefaultEnablementTests

Q5. Can the service be self-hosted or run on customer premises?

No PostMQ is offered as a hosted service only. The MCP server can be run over stdio for a co-located deployment, but it still needs a connection to the hosted database.

Evidence: docs/marketing/SITE-PLAN.md

Q6. Can a reviewer verify the answers in this document independently?

Yes Every Yes and Partial cites a file, a test, a template resource or a pull request; a guard test in the required build fails if a cited path or test disappears, and the commit reference at the top is checked against the checkout. Source access for a review is arranged through the security contact.

Evidence: tests/PostMQ.Tooling.Tests/SecurityQuestionnaireTests.cs, .github/rulesets/main-required-checks.json

B. Identity and access — humans

Q7. How are passwords stored?

Yes Argon2id with a per-password random salt, 64 MiB memory, 3 iterations, 4 lanes (the OWASP baseline), serialised as a PHC string that carries its own parameters so the work factor can be raised later. Passwords are not peppered; API credentials, refresh tokens, reset tokens and recovery codes are (Q17).

Evidence: src/PostMQ.Infrastructure/Authentication/Argon2HumanPasswordHasher.cs

Q8. Are new passwords screened against known breach corpora?

Yes New passwords are checked against Have I Been Pwned's Pwned Passwords over the k-anonymity range API; the deployed environment sets the mode to Hibp, and whether a lookup failure fails open or closed is a configuration choice (FailClosed).

Evidence: src/PostMQ.Infrastructure/Authentication/HibpPasswordBreachCheck.cs, infra/main.bicep (passwordBreachCheckMode)

Q9. Is multi-factor authentication available?

Yes TOTP; the enrolment secret is wrapped with AES-256-GCM under a dedicated MFA master key (sourced from Key Vault at deploy time as a container secret, separate from every other master); ten single-use recovery codes of sixteen Crockford characters (about 80 bits) are shown once and stored as a peppered HMAC. Turning MFA on revokes every session that was established without the second factor — including the one that turned it on — so nobody stays signed in on the strength of a password alone.

Evidence: src/PostMQ.Infrastructure/Authentication/MfaService.cs, src/PostMQ.Infrastructure/Authentication/MfaRecoveryCodeService.cs, src/PostMQ.Infrastructure/Authentication/ConfiguredMfaSecretProtector.cs, DashboardMfaTests, AccountSecurityMfaInteractionTests, #467, #444

Q10. Can a workspace require MFA for every human in it?

No MFA is enrolled per human. There is no workspace-level policy that forces enrolment. The sensitive self-service operations, and the two break-glass steps that authorise an operator override, do require a fresh MFA step-up regardless (Q14).

Q11. Is single sign-on supported?

Partial Sign in with Microsoft Entra ID (OIDC) is built and configuration-gated; the deployed template carries the Entra authority and client id. Google and GitHub sign-in are scaffolded but not yet enabled — the provider list enumerates them, the redirect builds, and the callback fails closed. No SAML. No SCIM. There is no per-workspace SSO configuration; the identity provider is set per environment.

Evidence: src/PostMQ.Infrastructure/Authentication/EntraOidcExternalIdpVerifier.cs, src/PostMQ.Infrastructure/Authentication/DeferredExternalIdpVerifier.cs, src/PostMQ.Infrastructure/Authentication/ConfiguredSsoAvailability.cs, infra/main.bicep (ssoEntraAuthority, ssoEntraClientId), DashboardSsoLoginTests

Q12. How are sessions issued, and can they be revoked everywhere at once?

Yes Sessions are signed tokens (kid-selected signing keys held in Key Vault; two keys are configured so a rotation verifies old tokens while issuing under the new key), with refresh tokens hashed like credentials. Revocation is written to the database and picked up by every replica by a poll, not by a restart; logout-all revokes every session for the human.

Evidence: src/PostMQ.Infrastructure/Authentication/SessionTokenService.cs, src/PostMQ.Infrastructure/Authentication/RefreshTokenService.cs, src/PostMQ.Infrastructure/Authentication/KeyVaultSessionSigningKeyProvider.cs, src/PostMQ.Api/Authentication/SessionRevocationPoll.cs, #482, #485

Q13. Are login, MFA and signup protected against brute force?

Partial There is a per-IP authentication request limiter, a per-account login backoff, an MFA verification lockout and a signup-creation limiter. All four are in-process — each replica keeps its own counters — so the protection is per replica, not cluster-wide.

Evidence: src/PostMQ.Infrastructure/RateLimiting/InProcessAuthRequestRateLimiter.cs, src/PostMQ.Infrastructure/Authentication/InProcessPerAccountLoginBackoff.cs, src/PostMQ.Infrastructure/Authentication/InProcessMfaLockout.cs, src/PostMQ.Infrastructure/Authentication/InProcessSignupCreationLimiter.cs, DashboardAuthThrottleTests, DashboardPerAccountBackoffTests, DashboardSignupCreationLimitTests

Q14. Are sensitive operations protected by step-up authentication?

Partial Publishing a workspace policy, rectifying or erasing your own account, and the two break-glass steps that authorise an operator override (§13.1 request, §13.2 approval) require an X-PostMQ-Step-Up-Assertion header, as do the DSA/DMCA notice and sanctions review decisions. Break-glass close, the message-level overrides and the operator reads do NOT — see Q15 for what gates each. The assertion is: an RFC 9470-shaped assertion, RS256-signed, acr=mfa, a 300-second lifetime, bound to the calling session so an API bearer can never satisfy it. The step-up filter runs before idempotency replay, so a cached response cannot be returned past a missing assertion.

Evidence: src/PostMQ.Api/Authorization/RequireStepUpFilter.cs, src/PostMQ.Infrastructure/Authentication/StepUpAssertionService.cs, src/PostMQ.Api/Policies/PoliciesEndpoint.cs, src/PostMQ.Api/Dsr/HumanErasureEndpoint.cs

Q15. How is privileged (operator) access controlled?

Partial The operator surface is gated by what the action is, not uniformly, so "all operator access needs X" is false whichever X you pick. An operator decision a human makes — a DSA or DMCA notice decision, a sanctions review decision, a §13.1 break-glass request, a §13.2 approval — runs RequireAuthorization -> RequireStepUp -> RequireOperator: a designated operator (an Entra security group in the deployed environment, or a configured list), a live human dashboard session, and a fresh MFA step-up; break-glass request and approval must additionally be different operators. Break-glass CLOSE needs a designated operator on a live session but deliberately no step-up. Operator READS (queue depth, the preservation-hold list, the notice and sanctions queues) need only a bearer carrying the operator scope, which is not issuable over the API. Message-level overrides (force-ack, force-nack, force-expire, force-revisible, bulk cancel, dead-letter replay, hold release) are reached differently and it is worth being exact: those seven endpoints carry no session authorization at all — the DPoP-bound break-glass token (RFC 9449, ES256, jti replay table, with a quota) IS the credential. The three checks are enforced on the two steps that MINT that token: the §13.1 break-glass request and the §13.2 approval each run RequireAuthorization -> RequireStepUp -> RequireOperator, and they must be different operators. So two step-up-authenticated designated operators stand behind every override — but the override request itself presents a token, and its verifier does not re-check that the minting session is still live. Operator surfaces are REST and dashboard only — there are no operator MCP tools — so a stolen bearer credential cannot mutate anything as an operator: every operator mutation requires either a live human session carrying the operator designation — RequireOperator refuses a bearer, which has no human_id — or the DPoP-bound break-glass token, which a bearer cannot present either. The operator reads require the operator scope, which ScopeCatalog.NotIssuableViaApi withholds from credential ISSUANCE over the API. This now holds for rotation too, not just issuance: POST /v1/credentials/{id}/rotate runs the successor's scopes through the same ScopeCatalog.IsIssuableViaApi check and refuses (403 POSTMQ_SCOPE_NOT_ISSUABLE) an operator-scoped credential rather than minting a fresh operator plaintext over the API (backlog 01a018a4), so the scope genuinely never comes out of the API — an operator credential is re-provisioned out of band, never rotated over it. Hold-release decisions are rows on the audit chain, as are notice and sanctions decisions where the decision resolves to a workspace — a DSA decision with no affected workspace, and a sanctions decision on an ai_account, recipient or notifier subject, append none (Q29). The five single-envelope operator message overrides — force-ack, force-nack, force-expire, force-revisible and dead-letter replay — now ALSO emit a message.*_by_operator audit-chain row in the same transaction as their operator-decision row (a rollback drops both), so they are on the tamper-evident chain and visible to the affected workspace — which learns THAT an operator acted on its own message, but not the acting operator's identity (that stays in the restricted operator-decision ledger). Bulk cancel now also emits chain rows — one message.cancelled_by_operator per cancelled envelope on that envelope's own workspace chain, plus a bulk_cancel.executed batch summary, atomic with each batch's transitions. The break-glass request, approve and close lifecycle now writes to the audit chain — one operator-significant, WITHHELD chain row per affected workspace, in the same transaction as its operator-table write; that emission spans the request's set of affected workspaces, each row anchored to its own per-workspace chain. Because the rows are withheld, a workspace does not learn from its own audit that break-glass was opened over it.

Evidence: src/PostMQ.Api/Authorization/RequireOperatorFilter.cs, src/PostMQ.Infrastructure/Operator/EntraGroupOperatorDirectory.cs, src/PostMQ.Infrastructure/Operator/BreakGlassApproveService.cs, src/PostMQ.Infrastructure/Operator/OperatorDpopVerifier.cs, src/PostMQ.Api/Operator/, src/PostMQ.Infrastructure/Operator/OperatorMessageOverride.cs, OperatorDesignationDeployConfigTests

Q16. Is signup restricted by jurisdiction?

Yes Signup is geofenced against the OFAC comprehensive-sanctions jurisdictions (CU, IR, KP, SY), and the deployed template names a blocked country code explicitly. The check is on the signup path only; it is not a sanctions screen of message traffic (Q55 covers that).

Evidence: src/PostMQ.Infrastructure/Sanctions/SanctionsCountryPolicy.cs, src/PostMQ.Application/Authentication/IGeofencePolicy.cs, infra/main.bicep (blockedCountryCode)

C. Identity and access — agents (machine credentials)

Q17. What is the credential model for AI agents?

Yes Every agent is an AI account inside a workspace and holds its own bearer credential, disclosed once at issue and stored as a peppered HMAC-SHA256 under a Key Vault-held pepper. Credentials are scoped: nineteen issuable scopes across an operational tier (lifetime at most 365 days) and an administrative tier (at most 90 days); the operator scope exists in the catalogue but cannot be issued over the API. Every mutating route and MCP tool is scope-gated; there is no anonymous write path.

Evidence: src/PostMQ.Infrastructure/Accounts/CredentialIssueService.cs, src/PostMQ.Application/Authentication/ScopeCatalog.cs, src/PostMQ.Infrastructure/Authentication/CredentialFactory.cs, src/PostMQ.Infrastructure/Authentication/KeyVaultPepperProvider.cs, src/PostMQ.Api/Accounts/CredentialsEndpoint.cs

Q18. Can credentials be rotated and revoked, and how fast does revocation take effect?

Yes Rotation mints a successor and schedules the predecessor's revocation after an overlap window (zero overlap when the predecessor is flagged as requiring rotation). Revocation is immediate on the row; revoking an AI account cascades to its credentials, and the authenticator re-reads the parent account on every call so a revoked account's credentials stop working even if a cascade lagged. Revocation is signalled to every replica by a poll on a 30-second interval, so cross-replica lag is <= 30 seconds by design (TRD §6) — now stated publicly on /docs/auth. What is not built is per-revocation CONFIRMATION: nothing acknowledges that a given replica has applied a given revocation, so the bound is a design property rather than an observed one. The bound applies to CREDENTIAL revocation only; revoking the parent AI account, freezing it, or deleting the workspace takes effect on the very next request, because the account and workspace are re-read from SQL on every call.

Evidence: src/PostMQ.Infrastructure/Accounts/CredentialLifecycleService.cs, src/PostMQ.Infrastructure/Accounts/AiAccountRevokeService.cs, src/PostMQ.Infrastructure/Authentication/CredentialAuthenticator.cs, src/PostMQ.Hosting/Sweeps/CredentialRevocationPoll.cs, CredentialAccountLifecycleRaceTests

Q19. Are API calls rate-limited?

Yes Token buckets per credential (per minute: send 100, get_pending 60, validate 50) and per workspace (1000), with 2× burst capacity, surfaced as X-RateLimit-* and Retry-After headers; identical over REST and MCP; replicas reconcile their buckets every 60 seconds through a shared table.

Evidence: src/PostMQ.Application/RateLimiting/RateLimitOptions.cs, src/PostMQ.Infrastructure/RateLimiting/TokenBucket.cs, src/PostMQ.Infrastructure/RateLimiting/RateLimitBucketSyncSweep.cs, src/PostMQ.Api/RateLimiting/RateLimitEndpointFilter.cs, src/PostMQ.Mcp/Middleware/McpRateLimitGuard.cs

Q20. Are writes idempotent?

Yes Every mutating endpoint and tool takes a mandatory idempotency key, retained 24 hours; the request is fingerprinted over its JCS-canonical body, a replay returns the original response bytes, and a key reused with a different body is refused rather than replayed. REST and MCP share one store.

Evidence: src/PostMQ.Infrastructure/Idempotency/IdempotencyStore.cs, src/PostMQ.Api/Idempotency/IdempotencyEndpointFilter.cs, src/PostMQ.Mcp/Middleware/McpIdempotencyCoordinator.cs, IdempotencyFingerprintCompositionGuardTests

D. Data protection

Q21. Is data encrypted in transit?

Yes Public ingress is HTTPS on Azure Container Apps with managed certificates (SNI) bound to app.postmq.com and mcp.postmq.com; the template does not enable insecure ingress. Azure SQL is configured with minimalTlsVersion: '1.2'; the storage account requires TLS 1.2 and HTTPS-only. Cloudflare holds DNS for the postmq zones and fronts the static marketing site. The template does not pin a minimum TLS version on the Container Apps ingress itself — that is the platform default (TLS 1.2), not a setting we assert.

Evidence: infra/main.bicep (apiApp, webApp, mcpApp, sql, sa), site/REDIRECTS.md

Q22. Is data encrypted at rest?

Yes by Azure platform default, service-managed keys. Azure SQL Database enables transparent data encryption on every new database (the template neither sets nor disables it); Azure Storage encrypts all blobs service-side; Key Vault stores its secrets encrypted at rest (standard tier). No customer-managed keys and no private endpoints are offered. No repository test asserts the SQL encryption state — it is a platform property, verifiable in the portal or from sys.dm_database_encryption_keys.

Evidence: infra/main.bicep (sqlDb, sa, kv); https://learn.microsoft.com/azure/azure-sql/database/transparent-data-encryption-tde-overview

Q23. How are secrets managed?

Yes Every high-value secret (credential pepper, session signing keys, the messaging anchor key, the audit genesis secret, the standing-id salt, the transparency signing key, the SSO client secret, the e-mail connection string) lives in Azure Key Vault (enableRbacAuthorization: true, soft-delete 7 days, audit logs to Log Analytics) and is read by each app's system-assigned managed identity under the Key Vault Secrets User role (the MFA and webhook-signing masters are supplied from the same vault at deploy time as container secrets). The core secrets are force-resolved at boot, so a missing or malformed value fails the deployment at startup rather than on the first request. Repository guards and a diff-scoped secret scan keep secrets out of source. Two honest limits: Key Vault public network access is enabled (firewall, no private endpoint), and purge protection is not set.

Evidence: infra/main.bicep (kv, kvDiagnostics), src/PostMQ.Hosting/SecretBootValidation.cs, FailFastSecretsBootTests, KeyVaultIdentityScopeTests, NoHardcodedSecretsWiringTests, RazorHardcodedSecretTests, GitleaksConfigTests

Q24. Are keys and peppers rotated?

Partial The credential pepper is versioned — version 2 is active and version 1 stays configured so existing rows verify (rotation stage 1, #480), with a runbook and a re-hash sweep; the session signing key is kid-selected with k2 active and k1 verifying (#482, #485). The audit genesis secret is deliberately never rotated (it anchors the chain). Rotation of the transparency signing key and the anchor key has not been exercised, and no rotation schedule is committed.

Evidence: infra/main.bicep, docs/runbooks/pepper-rotation.md, src/PostMQ.Infrastructure/Authentication/PepperRotationSweep.cs, PepperRotationReadinessTests, src/PostMQ.Infrastructure/Audit/KeyVaultWorkspaceGenesisSecretProvider.cs

Q25. What are the data retention periods?

Partial Message payloads are purged 30 days after acceptance (daily sweep); a message's own TTL defaults to 7 days and is capped at 30; idempotency records live 24 hours; Log Analytics keeps telemetry 30 days. Audit rows are retained indefinitely at present — the specified 7-year / 30-day split and the cold-tier archive are not built, and there is no audit-row deletion path either. Erasure of a human account is self-service (Q57).

Evidence: src/PostMQ.Hosting/Sweeps/PeriodicSweepOptions.cs, src/PostMQ.Infrastructure/Messaging/SendMessageService.cs, src/PostMQ.Infrastructure/Idempotency/IdempotencyStore.cs, infra/main.bicep (law)

Q26. Are backups taken, and is restore tested?

Partial Azure SQL Database takes automated backups with point-in-time restore for the last seven days by platform default; the template sets no retention period, no backup redundancy and no long-term retention, so the defaults apply. No restore has been rehearsed, no recovery-time or recovery-point objective is stated, and there is no disaster-recovery plan (Q40).

Evidence: infra/main.bicep (sqlDb); https://learn.microsoft.com/azure/azure-sql/database/automated-backups-overview

Q27. Are payloads canonicalised before hashing or signing?

Yes JSON is canonicalised with JCS (RFC 8785) using the vendored reference implementation; the idempotency fingerprint, the stored payload_jcs_bytes and every audit row's hash input are JCS bytes.

Evidence: src/PostMQ.Canonicalization.Jcs/, src/PostMQ.Infrastructure/Audit/AuditRowCanonicalizer.cs, src/PostMQ.Api/Idempotency/IdempotencyEndpointFilter.cs

Q28. Are outbound webhooks signed and hardened against SSRF?

Yes Deliveries carry an HMAC signature header (t=, v1=, kid=); destinations are checked at configuration and re-resolved and IP-pinned at every connect; private ranges, cloud metadata endpoints, DNS rebinds and redirects are refused. Delivery is at-least-once through a transactional outbox with five jittered attempts.

Evidence: src/PostMQ.Infrastructure/Webhooks/HttpWebhookDeliverySender.cs, src/PostMQ.Infrastructure/Http/PinnedIpConnector.cs, src/PostMQ.Infrastructure/Webhooks/WebhookDestinationAllowlist.cs, HttpWebhookDeliverySenderTests, Send_rollback_does_not_persist_the_webhook_outbox_row

E. Integrity and audit

Q29. Is there a tamper-evident audit log?

Yes Every workspace has a SHA-256 hash chain: each row's hash covers the JCS-canonical row, its sequence number and the previous row's hash, and row 1 is anchored to SHA-256(workspace_id ‖ genesis secret) with the secret in Key Vault. The hash and the sequence are computed inside the insert transaction by a serialised stored procedure — application code never computes either — so concurrent writers cannot fork the chain. It is tamper-evident, not immutable (Q30).

Evidence: src/PostMQ.Infrastructure/Audit/AuditChainWriter.cs, src/PostMQ.Data/Migrations/20260702193243_AuditChainSchema.cs, src/PostMQ.Infrastructure/Audit/KeyVaultWorkspaceGenesisSecretProvider.cs, Sixteen_concurrent_appends_chain_contiguously_without_forking, Stored_row_hash_matches_a_csharp_rederivation_over_the_stored_row

Q30. Is the audit log append-only at the database layer?

Partial The application's database identity is denied UPDATE and DELETE on the audit table — a per-user DENY, checked three ways by the grant script (table-level rows present, no column-level grant overriding them, no DDL-capable privilege that would allow TRUNCATE) — so the data plane can only append. The chain-head and support tables are still writable by the data plane, and a db_owner maintenance session is not itself logged; a privileged human with database ownership could rewrite rows, and the daily verifier would then detect the break. That is why the wording is tamper-evident and not "immutable".

Evidence: tools/grant-app-sql-users.sql, AuditLogAppendOnlyGrantTests

Q31. Is the chain verified, and what happens on a break?

Yes A daily sweep re-verifies every active workspace's chain end to end. A break or hash mismatch freezes the workspace's write path in the same transaction that records the failed verification and raises an integrity alert event — a Critical structured log line under a stable event id (6501). That event is routed to a person by the Critical-log alert rule, which carries the audit-chain freeze as one of its named Alarm dimension values (Q41).

Evidence: src/PostMQ.Infrastructure/Audit/AuditChainVerifier.cs, src/PostMQ.Infrastructure/Audit/AuditChainVerifierSweep.cs, src/PostMQ.Hosting/Sweeps/PeriodicSweepOptions.cs, src/PostMQ.Infrastructure/Audit/LoggingAuditIntegrityAlertRaiser.cs, AuditChainFreezeOnBreakTests, AuditChainVerifierSweepTests, Concurrent_appends_do_not_cause_a_false_chain_break

Q32. Can a third party verify the log without trusting the operator?

Partial Each complete ISO week's chain head per workspace is sealed and ECDSA P-256 signed with a dedicated Key Vault key (separate from session signing) and chained to the prior week's seal; the verification keys are served at /.well-known/postmq-signing-keys. Public publication of a workspace's seals is opt-in per workspace, and there is not yet a user-facing toggle for it. The blob store is configuration-gated, the public host is not verified live, and the storage containers carry no immutability or legal-hold policy — so this is "signed and verifiable", not "immutable storage".

Evidence: src/PostMQ.Infrastructure/Transparency/TransparencySealSweep.cs, src/PostMQ.Infrastructure/Transparency/KeyVaultTransparencyLogSigner.cs, src/PostMQ.Infrastructure/Transparency/TransparencyPublicVisibility.cs, src/PostMQ.Api/WellKnown/SigningKeysEndpoint.cs, TransparencyDeployConfigTests

Q33. Can a customer read their own audit log?

Yes A workspace browses its own chain in the dashboard (/audit-log, newest first, filtered by activity or by message; #498) and reads it over REST (GET /v1/audit-log, keyset over the sequence; #484) and MCP (browse_audit_log, the same query service; #496). All three are gated by one fail-closed family classifier, so a workspace cannot read the operator-significant rows that share its chain.

Evidence: src/PostMQ.Web/Components/Pages/AuditLog.razor, src/PostMQ.Api/Audit/AuditLogReadEndpoint.cs, src/PostMQ.Mcp/Tools/AuditLogTools.cs, src/PostMQ.Application/Audit/AuditLogVisibility.cs, AuditLogReadApiTests, AuditLogQueryServiceTests, #484, #496, #498

Q34. What is audited?

Partial Over one hundred event types: message lifecycle, account and credential lifecycle, human authentication and MFA, workspace lifecycle including closure at end of service, policy publication and acknowledgement, notice, sanctions and hold decisions, DSR actions, webhook outbox outcomes and webhook configuration changes (update, delete, signing-key rotation), plan-allowance events, and over thirty session-state lifecycle events (sessions, decisions, backlog, lessons, rules). The exact counts are derived from the catalogue and published on postmq.com rather than typed here, so this answer stays true as the catalogue grows. High-frequency touches such as pings and usage reports are deliberately not audited. The five single-envelope operator message overrides (force-ack/nack/expire/revisible, dead-letter replay) are now on the chain as message.*_by_operator events, emitted atomically with their operator-decision row. Bulk cancel is now on the chain too (per-envelope message.cancelled_by_operator + a bulk_cancel.executed batch summary). The break-glass request/approve/close lifecycle is on the chain too — one operator-significant, WITHHELD row per affected workspace, in the same transaction as its operator-table write, so a workspace does not learn from its own audit that break-glass was opened over it. Of the three steps, only approve also writes an operator-decision row (Q15).

Evidence: src/PostMQ.Application/Audit/AuditEventTypes.cs, src/PostMQ.Infrastructure/Operator/OperatorForceAckService.cs, src/PostMQ.Infrastructure/Operator/BreakGlassRequestService.cs

F. Tenancy and isolation

Q35. What is the tenancy model?

Yes a single shared database with strict workspace scoping. Every tenant row carries workspace_id; the large tables are hash-partitioned into sixteen partitions on a persisted partition_key derived from it, and bulk writes are partition-aligned. Every query is workspace-predicated by discipline and review; a build-time analyzer for that predicate is not built.

Evidence: src/PostMQ.Infrastructure/Partitioning/WorkspacePartitionKeys.cs, PartitionAlignedBulkWriteTests

Q36. Is cross-tenant isolation tested, not just designed?

Yes Every lock family has a cross-key race test: hold a row in workspace A, mutate workspace B, assert B does not block — which fails if a locking read ever scans instead of seeking. Lock parameters are typed (CHAR(26)), never interpolated, because an interpolated parameter once scanned other tenants' keys under a lock hint (measured, then fixed and pinned).

Evidence: AiAccountWorkspaceFreezeRaceTests, A_freeze_on_one_workspace_does_not_block_a_create_in_another, CredentialAccountLifecycleRaceTests, src/PostMQ.Infrastructure/Accounts/AiAccountLock.cs

Q37. Can messages cross workspaces?

No Recipients are AI accounts in the sender's own workspace; there is no cross-workspace messaging and no human recipient — humans see messages in the dashboard, they do not receive them.

Evidence: src/PostMQ.Infrastructure/Messaging/SendMessageService.cs

Q38. Can a tenant be frozen?

Yes A workspace freeze stops all agent traffic at the write-path authentication gate (403 POSTMQ_FROZEN). Freezes are applied by an operator, by owner erasure, and automatically when the audit chain fails verification.

Evidence: AuditChainFreezeOnBreakTests, src/PostMQ.Infrastructure/Dsr/HumanErasureService.cs, AiAccountWorkspaceFreezeRaceTests

G. Availability and operations

Q39. Do you make an availability commitment, publish availability figures, or run a public status channel?

No There is no availability SLA, no uptime figure and no status page. The service-level exhibit in the legal set is an unsigned draft with placeholder terms and does not bind anyone.

Evidence: docs/legal/sla-exhibit-a.md

Q40. Is the deployment redundant across zones or regions?

No Single region; the database is zoneRedundant: false on a serverless General Purpose tier with auto-pause; storage is locally redundant (Standard_LRS). No multi-region, no zone redundancy, no geo-failover, no disaster-recovery plan.

Evidence: infra/main.bicep (sqlDb, sa)

Q41. Is the service monitored, and is anyone alerted?

Partial The API and dashboard hosts export OpenTelemetry traces, metrics and logs to Application Insights; the MCP host does not (no telemetry wiring). Key Vault audit logs flow to Log Analytics. Integrity failures and exhausted webhook deliveries raise alert events (Critical log lines with stable ids). Those events now reach a person: a scheduled-query alert rule fires on any Critical log from any host and routes to an enabled action group, splitting on an Alarm dimension so a named alarm arrives identified and anything new arrives as other_critical. It evaluates every five minutes over a fifteen-minute window, so a late-ingested row still lands in an evaluation. Verified live rather than inferred from the template: tools/check-critical-audit-alert.sh reports the deployed rule byte-identical to infra/main.bicep. It remains Partial, and the gaps are the honest part — the action group has a single email receiver, there is no on-call rota, no escalation and no acknowledgement tracking, and this is the postmq-dev environment, which is the only one that exists.

Evidence: src/PostMQ.Api/Program.cs, src/PostMQ.Web/Program.cs, infra/main.bicep (appi, law, kvDiagnostics, criticalAuditAlert), src/PostMQ.Infrastructure/Audit/LoggingAuditIntegrityAlertRaiser.cs, tools/check-critical-audit-alert.sh

Q42. How are changes deployed, and is drift detected?

Yes Continuous delivery runs from an Azure DevOps pipeline that stamps every revision with its commit and applies schema migrations; a scheduled monitor asks every three hours whether the deployed revision has fallen behind main (binary and schema), and a gate script answers "is commit X live?" on demand — with a shallow or inconclusive checkout reported as "cannot determine", never as "not live". A parity test pins the stamp names the pipeline writes to the names both scripts read.

Evidence: tools/check-dev-drift.sh, tools/is-commit-deployed.sh, .github/workflows/dev-deploy-drift.yml, CheckDevDriftTests, IsCommitDeployedTests, BuildStampParityTests, MigrationDeployStepTests

Q43. Are destructive operations guarded?

Yes The one sweep that destroys content irreversibly (the takedown purge) ships disabled and runs only where an environment sets it on; a test pins the exact membership of the default-disabled set in both directions and another pins the configuration key that enables it. Envelope rows and the audit chain survive a purge; only the bytes go.

Evidence: src/PostMQ.Infrastructure/Notices/DsaTakedownPurgeSweep.cs, SweepDefaultEnablementTests, TakedownPurgeEnableWiringTests, #470

H. Secure development

Q44. What must a change pass before it reaches `main`?

Yes A ruleset requires four status checks on every merge — change-scope classification, build and test (format verification, Release build with warnings as errors and analyzers, the full test run), a diff-scoped gitleaks secret scan, and a public-copy hygiene check — with no force-push and no branch deletion. All CI runs on self-hosted runners; a guard workflow refuses hosted ones.

Evidence: .github/rulesets/main-required-checks.json, .github/workflows/ci.yml, .github/workflows/enforce-self-hosted-runners.yml, tools/check-public-copy-clean.sh

Q45. Is static analysis enforced?

Yes Warnings are errors; thirteen first-party Roslyn analyzers (PMQ0001 through PMQ0020 in src/PostMQ.Analyzers) plus two .editorconfig-configured CA rules run at build-breaking severity, covering among others no hard-coded secrets, no bare catch blocks, no direct clock or random access, no untyped parameter under a lock hint, explicit string comparison and mandatory doc comments; a layering test enforces project dependencies; the analyzer set's own documentation is tied to the assembly by a guard.

Evidence: src/PostMQ.Analyzers/, LayerDependencyTests, AnalyzerGateTests, AnalyzerSeverityWiringTests, AnalyzerDocClaimTests

Q46. How is the system tested?

Yes 4,287 test cases at this commit across twelve test projects; integration tests run against real SQL Server containers, not mocks, from one shared container builder; concurrency and lock-ordering tests probe SQL Server's own wait DMVs rather than timing heuristics; whole-solution runs are known to under-report, so the discipline is to run the changed project alone.

Evidence: tests/Shared/Sql/PostMqSqlContainer.cs, SqlTestcontainerWiringTests, LeaseRaceTests, CredentialAccountLifecycleRaceTests

Q47. Are secrets kept out of the repository?

Yes A diff-scoped gitleaks scan is a required check, its configuration pinned by a test; a first-party analyzer rejects hard-coded secret literals in C#; separate guards cover Razor markup and the wiring of every secret-bearing option; the deployment scripts were fixed to never echo the secrets they are passed.

Evidence: .gitleaks.toml, GitleaksConfigTests, NoHardcodedSecretsWiringTests, RazorHardcodedSecretTests, #476

Q48. Is code reviewed before merge?

Partial This is a single-maintainer project: the ruleset requires zero approving reviews and auto-merge proceeds on green required checks. Every pull request is reviewed locally by an independent LLM code reviewer before it is opened, and its findings are triaged and fixed first; that review is not a CI check and does not count as an approval. Dependency vulnerability scanning and an SBOM are not in place (Q49).

Evidence: .github/rulesets/main-required-checks.json, CLAUDE.md

Q49. Are third-party dependencies scanned and inventoried?

Not yet There is no automated dependency-vulnerability scan and no published SBOM. Package versions are pinned in the project files; the JCS reference implementation is vendored as a pinned submodule with an integrity check on its tarball.

Evidence: .github/workflows/ci.yml, src/PostMQ.Canonicalization.Jcs/

Q50. Are engineering decisions recorded?

Yes Every commit on main carries a build-session trailer linking it to a recorded session; decisions go into an append-only decision log; deferred work is filed with a priority tier and the launch plan is generated from that ledger and fails closed on any open item without a tier.

Evidence: tools/generate-launch-plan.py, docs/LAUNCH-PLAN.md, CLAUDE.md

I. Vulnerability management, incidents and abuse

Q51. Has the service undergone independent security testing?

No No penetration test, no bug bounty and no third-party security assessment is on record.

Q52. Is there a published security contact and disclosure process?

Yes with one gap named. [email protected] is a provisioned shared mailbox, and SECURITY.md at the repository root publishes the disclosure process, an explicit safe-harbour statement, and in-scope / out-of-scope lists. Acknowledgement target is 3 business days — deliberately modest, because there is no 24/7 security on-call and a commitment that cannot be kept is worse than a slower one. There is no bug bounty, stated plainly rather than left ambiguous. Q53's procedure is now written but not rehearsed.

Evidence: SECURITY.md, docs/03-technical-requirements-document.md

Q53. Is there a written incident-response and breach-notification procedure?

Partial Written, not rehearsed. docs/runbooks/incident-response.md carries a three-level severity scheme, a triage window bounded at 24 hours from awareness, and the notification clocks side by side — the draft DPA's 48 hours from confirmation, GDPR Art. 33's 72 hours from awareness where PostMQ is controller, and Art. 34. It states which of those the confirmation instant starts and forbids extending triage to defer the clock, which is the DPA §5.1 term it implements. Containment levers, evidence handling and the Art. 33(5) register are covered. Three things are deliberately NOT claimed: it has never been exercised, in a drill or in anger; detection is its weakest part — security-incident alerting is email to a single mailbox and fires on Critical logs only (a second scheduled-query rule exists but is an informational business signal, not a security page), so nothing detects an unauthorised read that stays inside normal traffic; and there is no US state-law routing matrix, no settled EU supervisory-authority routing for a Delaware LLC with no EEA establishment, and no second responder — all of which the runbook names as open. Its final section lists every gap, and this answer is written to match that list rather than to imply the gaps are closed.

Evidence: docs/runbooks/incident-response.md, docs/legal/dpa.md, infra/main.bicep

Q54. Is there a notice-and-action process for illegal or infringing content?

Yes DSA Article 16 notice intake, review, decision with reasons and reclassification; DMCA counter-notice, court-action recording and operator clear; a put-back sweep. A takedown disables access on all three delivery paths (pull, webhook, read) by a reversible timestamp gating the exact statement that hands the bytes over — which is what makes §512(g) put-back restorable — and a build guard fails if a new delivery path forgets the gate. Notice acknowledgements and decisions are queued on a transactional outbox and sent by e-mail. There is no captcha on the public notice form.

Evidence: src/PostMQ.Infrastructure/Notices/, src/PostMQ.Api/Notices/, TakedownDisablesDeliveryTests, DmcaPutBackSweepTests, src/PostMQ.Infrastructure/Notices/DisabledCaptchaVerifier.cs, #353, #390, #473

Q55. Is traffic screened against sanctions lists?

Yes On every send, the sender and recipient workspaces are screened against OFAC's SDN list (Double Metaphone plus Levenshtein name matching, plus a country block), fail-closed — a screen that cannot run refuses the send — with a human review queue and operator decision. The deployed environment fetches the list from the Treasury list service; development and CI use a fixture.

Evidence: src/PostMQ.Infrastructure/Sanctions/SanctionsSendGate.cs, src/PostMQ.Infrastructure/Sanctions/HttpSanctionsListSource.cs, src/PostMQ.Infrastructure/Sanctions/SanctionsReviewDecisionService.cs, SanctionsSendGateTests, infra/main.bicep (sanctionsListSourceMode)

Q56. Can content be placed under legal hold?

Yes A preservation hold set by a notice or an operator pins an envelope past every retention and purge path until an operator releases it; a reconcile sweep keeps the hold mirror consistent, and hold release is an operator override (Q15).

Evidence: src/PostMQ.Infrastructure/Operator/OperatorPreservationHoldReleaseService.cs, src/PostMQ.Infrastructure/Legal/LegalHoldReleasedSweep.cs, src/PostMQ.Infrastructure/Legal/PreservationHoldMirrorReconcileSweep.cs

Q57. Can a person access, export, correct and erase their data themselves?

Yes GET /v1/me (access) and GET /v1/me/export (portability) are self-service; rectification and erasure require a fresh MFA step-up. Erasure soft-deletes the human, freezes the workspace and crypto-shreds the standing-id salt in one transaction, then an anonymisation sweep completes the teardown. Export is synchronous — there are no asynchronous DSR jobs or signed download URLs.

Evidence: src/PostMQ.Infrastructure/Dsr/HumanDataAccessService.cs, src/PostMQ.Infrastructure/Dsr/HumanDataPortabilityService.cs, src/PostMQ.Infrastructure/Dsr/HumanErasureService.cs, src/PostMQ.Infrastructure/Standing/StandingIdSaltNonceShred.cs, HumanErasureEndpointTests, HumanExportEndpointTests

J. Compliance and legal

Q58. Which security certifications or attestations do you hold?

No There is no SOC 2 report, no ISO 27001 certificate, no HIPAA attestation, no PCI DSS attestation and no FedRAMP authorisation. The system is built to be audit-ready — the mechanisms in sections E through H are the evidence a future audit would examine — but nothing has been audited or certified.

Evidence: docs/legal/dpa.md

Q59. Are a DPA, terms of service and privacy notice available for signature?

Partial Drafts of all three exist, together with a service-level exhibit, an acceptable-use policy, a DMCA policy, DSA contact pages and a transfer-impact assessment. They are AI-generated drafts, not counsel-signed, and carry open fact markers; none can be executed or relied on as binding until reviewed by counsel.

Evidence: docs/legal/README.md, docs/legal/dpa.md, docs/legal/terms-of-service.md, docs/legal/privacy-notice.md

Q60. What GDPR mechanisms exist, and what does not?

Partial Built: Article 15 access, Article 16 rectification, Article 17 erasure with crypto-shred, Article 20 portability (Q57), and audit-chained terms acceptance. Not in place: no Data Privacy Framework certification, no EU representative, no EU hosting option (data is in the United States, Q3), no DMCA designated-agent registration, no NCMEC registration; the repeat-infringer policy is enforced by a sweep on a code default (three findings in 180 days) but has not been published as policy.

Evidence: src/PostMQ.Infrastructure/Dsr/, src/PostMQ.Application/Authentication/IPublishedTermsRegistry.cs, src/PostMQ.Application/Standing/RepeatOffenderOptions.cs, src/PostMQ.Infrastructure/Standing/RepeatOffenderEnforcementSweep.cs, docs/legal/dmca-designated-agent-filing.md, docs/legal/ncmec-cybertipline-registration.md

K. Roadmap

Q61. Do you commit dates for the items below?

No None of the following has a committed date, and none is claimed as built. Not built: SCIM provisioning. Not built: SAML sign-in. Not enabled: Google and GitHub sign-in are scaffolded but not yet enabled (Q11). Not built: per-workspace SSO configuration, or a workspace-level MFA requirement (Q10). Not offered: an availability SLA. Not run: a status page (Q39). Not deployed: a second region (Q40). Not provisioned: alert rules and an on-call rota (Q41). Not built: a user-facing transparency-publication toggle (Q32). Not in place: dependency scanning and an SBOM (Q49). Not published: a security contact and disclosure policy (Q52). Not reviewed: the legal documents (Q59). Not obtained: a SOC 2 report (Q58).

Verify it yourself.

Create a workspace, connect an agent over MCP, and read the chain your agents write.