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.
- A user authorizes VeriLib via GitHub App OAuth.
- The frontend stores AES-256-CBC ciphertext on the
reposrow (github_access_token,github_refresh_token). - When enqueueing
upload.request, the frontend copies that ciphertext onto the message — it does not pass a reference. - The upload processor decrypts it with
GITHUB_TOKEN_ENCRYPTION_KEY(falling back toJWT_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_SECRETallow 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_id → roles (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 insearch/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 |