What we do.
A structured internal review against an established checklist, every quarter, plus immediately after any non-trivial dependency bump or auth-surface change. The checklist follows the OWASP ASVS Level 2 + OWASP API Security Top 10 framing — we don't claim certification, but we use the framework's questions verbatim where useful. Each pass produces a dated report committed to the repository, evidenced by file paths and test files, with findings either remediated in a referenced commit or accepted as residual risk with rationale.
- Master checklist — eight scope areas: authentication and session, public viewer + JSON-LD endpoint, dashboard write paths, storage, custom domains, dependencies, secrets, logging and monitoring.
- Quarterly review — every row evidenced in line with file paths and test files; pass / partial / fail call per row. Findings either remediated in a referenced commit or accepted as residual risk with explicit rationale.
- Dependency hygiene —
pip-auditon every review pass. Pinnedrequirements.txtwith no version ranges; bumps are deliberate and tested.
The load-bearing controls.
Concrete, verifiable claims — every one of these is a property of code or configuration a reviewer can check.
- Encrypted in transit. TLS on every public surface (qrregistry.io, qrregistry.eu, custom-domain viewers) via Caddy with on-demand certificate issuance. The session cookie is
HttpOnly+Secure+SameSite=Lax. - Encrypted at rest. PostgreSQL + MinIO on EU hardware with disk-level encryption.
- No passwords. Magic-link only. The raw token is ~256 bits of CSPRNG entropy, expires in 15 minutes, and is consumed on first use; the database stores only a SHA-256 hash, so a database read on its own can't sign anyone in.
- Differentiated audience auth. Auditor / recycler / repairer / notified-body credentials are short-lived JWTs revocable from a single admin surface; revocation takes effect on the next request, not at JWT TTL expiry.
- Ownership-checked write paths. Every passport, attestation, domain, and asset write is gated server-side on
row.user_id == user.id. Other users' rows return404rather than403— we don't leak existence. - Restricted-tier filter. A single function (
strip_to_visible) gates every passport surface — HTML viewer, JSON-LD endpoint, audit feed, PDF. No field can leak through one branch and not the others. - Append-only audit chain. Every save writes a
passport_revisionsrow with hash_before / hash_after / structured diff.DELETE /dashboard/passports/{id}returns403unconditionally — Article 10(4) lifetime persistence. - Custom-domain allowlist. Caddy's on-demand TLS ask-endpoint is bound to loopback and consults the verified-domains table; only verified-and-provisioned hostnames issue a certificate. Single-claim invariant — a hostname can only ever belong to one owner.
- Secrets discipline. No secrets in source.
.envis the only inventory; per-service env vars (database, Stripe, Resend, MinIO) are independent. - Privacy-preserving analytics. Scan events record an ISO country code (header-derived, never raw IP) and a coarse device class — never a tracking cookie, never a credential identity. Documented and surfaced under
/privacy.
What we explicitly don't do.
We don't pay for a third-party penetration test until a customer's procurement gate makes it justified cost. ESPR Article 11(h) doesn't define a certification path or mandate third-party testing; commissioning a €15-30k engagement buys credibility but no regulatory <code>✅</code>, and the dated internal review documents the same evidence at a lower cost while the company is small.
We don't claim SOC 2 or ISO 27001 certification. Our infrastructure provider's posture is what's behind the <em>“ISO 27001 aligned”</em> footer line; the certification belongs to them, not to Contenza K/S.
We don't pretend a clean review is the absence of findings. The 2026-04-30 review surfaces five 🟡 partial rows — four are dependency CVEs with available fix versions tracked as a follow-up bump, and one is a deferred application-layer rate-limit. Honest is more useful than aspirational.
What you can read.
- The master checklist lives at
docs/security-review-checklist.mdin the source repository — every row is a question the next review must answer with evidence. - Every dated review pass lives at
docs/security-review-{YYYY-MM-DD}.md— file paths, test files, and findings, in line. The git history shows what changed between passes. - Every public claim on this page maps to a row in the most recent review.
Reporting a vulnerability.
Please email the team with details. We treat security reports as priority and respond within one business day. There is no formal bug bounty programme yet — at our scale, recognition is a heartfelt thank-you and a place in the changelog.
Questions on this policy? Use the contact form — or email the team through the details on the contact page.