Skip to content

Security

Trust boundaries, credential flow, and the properties the architecture deliberately maintains.

Reporting a vulnerability

Do not open a public GitHub issue for a security vulnerability. Report privately through the appropriate channel for the affected repository. See Bugs.

Trust boundaries

flowchart TB
  subgraph pub[Public internet]
    br[Browser]
    cli[verilib-cli]
  end
  subgraph app[Application tier]
    php[PHP / Apache]
  end
  subgraph data[Data plane]
    db[(MySQL on Amazon RDS<br/>not publicly accessible)]
    mq[RabbitMQ]
    s3[(S3)]
  end
  subgraph work[Worker tier — no inbound]
    at[atomize / upload processors]
    ce[cert workers]
  end
  br --> php
  cli --> php
  php --> db
  php --> mq
  mq --> at
  mq --> ce
  at --> db
  at --> s3
  ce --> s3
Boundary Property
Only the frontend is internet-facing. Workers have no ALB and accept no inbound connections. They reach out to the broker and S3.
The frontend is the sole writer for certificates and product data. A compromised worker cannot write certificate rows — it can only publish a response the frontend chooses to apply.
Cert workers are DB-free. They hold no database credentials at all. Not an omission — a designed reduction of credential surface.
The database is not internet-reachable. MySQL runs on Amazon RDS with public access disabled and inbound 3306 allowed only from the app and worker security groups. Operator access goes through a tunnel from an allowed host, never a public endpoint. See Managed.
Probes run in containers. Untrusted repository code is compiled and analyzed inside a probe container, not on the worker directly.

Credential flow: private GitHub repos

This is the most security-relevant path in the platform, and it is deliberately unusual.

  1. A user authorizes VeriLib via GitHub App OAuth.
  2. The frontend stores AES-256-CBC ciphertext on the repos row (github_access_token, github_refresh_token).
  3. When enqueueing upload.request, the frontend copies that ciphertext onto the message — it does not pass a reference.
  4. The upload processor decrypts it with GITHUB_TOKEN_ENCRYPTION_KEY (falling back to JWT_KEY) and uses it for the clone.

Properties worth noting:

  • The upload processor never queries MySQL for tokens — it has no DB access on this path.
  • Optional GITHUB_APP_CLIENT_ID / GITHUB_APP_CLIENT_SECRET allow in-memory token refresh during a clone; refreshed tokens are not written back to the DB.
  • The atomize processor reads S3 only and never sees a token.

Ciphertext on the wire means the broker is in scope

Because tokens travel on the message body, anyone who can read the RabbitMQ queue holds ciphertext for every private repo currently being uploaded. That is safe only while GITHUB_TOKEN_ENCRYPTION_KEY stays secret and RABBITMQ_TLS is enabled in transit. Treat broker access as equivalent to token-vault access.

The encryption key is shared between the frontend and the upload processor and must be rotated on both simultaneously — see Rotate credentials.

Authorization

Layer Mechanism
Session Cookie session auth; /v2 endpoints assume withCredentials: true
Coarse role users.role_idroles (1 admin, 2 moderator, 3 user). See Roles
Fine-grained capability tasks + permissions tables. See Permissions

Role alone does not gate the sensitive flows. Certify checks Users::userCanCertify(...) for the Certifier task permission and returns 403 without it. Feature Repo is gated the same way.

CLI authentication uses API keys stored in apikeys, held client-side in the system keyring (macOS Keychain, Windows Credential Manager) or a file on Linux. See CLI config and files.

Secret storage

Secret Where it lives Never
RabbitMQ user/password AWS Secrets Manager, referenced by RABBITMQ_SECRET_ARN In deploy/config/<env>.env
MySQL (RDS) user/password DB_USER / DB_PASSWORD in the host's gitignored .env, restricted to the PHP-FPM user (chmod 640) Committed, world-readable, or shared across environments
S3 access ECS task IAM role — no keys in production Long-lived access keys
GITHUB_TOKEN_ENCRYPTION_KEY / JWT_KEY Environment, matched across two repos Committed
SEPOLIA_PRIVATE_KEY / MAINNET_PRIVATE_KEY Environment; mainnet authority is a Gnosis Safe Committed
GitHub PAT for artifact download ~/.config/verilib/gh-token on the deploy host Committed
Docs internal pages Names and locations only Values, ever

deploy/config/<env>.env is for non-secret configuration. Real env files are gitignored; only example.env is tracked.

On-chain

Anchoring authority is not a single key. Certify.certify() reverts unless the sender is the contract's immutable AUTHORIZED_CERTIFIER, which on mainnet is the BAIF Gnosis Safe — so mainnet anchoring requires multisig approval rather than one compromised key. Details: Certify contract.

The mainnet dedup scan defaults fail-closed so an unverifiable state retries rather than risks a duplicate anchor.

Documentation site

The docs themselves have a public/internal split, since MkDocs produces static HTML with no built-in auth:

Build Config Contains
Public mkdocs.yml docs/internal/ excluded via exclude_docs
Internal mkdocs-internal.yml Everything, served behind OAuth2 Proxy
  • Use OAuth2 Proxy v7.11.0 or later (CVE-2025-54576 — auth bypass).
  • Internal pages document secret names and locations only, never values.
  • Verify the split after nav changes: a public build must contain no internal/ paths, including in search/search_index.json.

Known gaps

Gap Impact
No SECURITY.md or published disclosure policy in the platform repos Reporters have no documented private channel
No documented key-rotation runbook Rotate credentials is a stub
verilib-atomizer, local_validate, probe have no LICENSE Not a vulnerability, but a legal exposure for a project inviting external use. See License
No dependency-scanning or SBOM documented Unknown third-party exposure