Security posture update — HttpOnly sessions, nonce-based CSP, SBOM pipeline
A transparent write-up of the April 2026 security hardening on sentrikat.com and portal.sentrikat.com: what we changed, why it matters, and what's next.
When a European security team evaluates a vulnerability-management vendor, one of the first things they look at is the vendor’s own security posture. That’s fair — we ship an agent into their environment, we hold their vulnerability data, and we sign NIS2 reports on their behalf. So we need to practise what we preach.
This post is a transparent summary of the changes we shipped in mid-April 2026 after an external audit of the SentriKat web platform. No marketing — just what changed, why, and how to verify it.
TL;DR
| Change | Before | After |
|---|---|---|
| Portal session storage | JWT in localStorage | HttpOnly + Secure + SameSite=Strict cookie |
| Portal CSRF defence | None | SameSite=Strict + X-Portal-Request marker header |
| Content-Security-Policy (landing + portal) | script-src 'unsafe-inline' | Per-request 'nonce-$request_id' |
| CSP violation reports | Silent | report-to + /api/security/csp-report endpoint |
| Python dependency integrity | Version pins only | pip-compile --generate-hashes workflow |
| NVD monitoring alerts | Instant on any 503 | Only after two consecutive failed probes |
| Public status page | 90-day history reset on restart | Persistent history + background probe scheduler |
| Trust center | Single /security page | /security, /compliance, /roadmap, /sbom, /disclosure, /subprocessors, /status |
Everything below is implemented and live.
Why move the portal session into a cookie
The old flow stored the JWT in browser localStorage after login. It’s a
common pattern for SPAs, but it has a well-known risk: any JavaScript
running on the page can read it. If an attacker lands a single XSS —
through a reflected URL parameter, a third-party script with a vulnerability,
or an admin screen that renders user input unsafely — they can exfiltrate
the session token in a fetch call and impersonate the user from their own
machine.
Moving the session into an HttpOnly cookie removes that escalation path entirely. JavaScript cannot read the cookie. Browser automatically sends it on subsequent requests. An XSS is still a bug to fix, but it no longer immediately becomes a session-theft chain.
SameSite=Strict blocks the classic cross-site POST attack that cookies
would otherwise enable. For defence-in-depth, every state-changing portal
request must also carry the custom header X-Portal-Request: 1 — a header
that browsers refuse to set on simple cross-origin requests, so an
attacker page cannot forge it.
Nonce-based CSP — without breaking Astro
Content-Security-Policy is a second layer: even if XSS code executes,
the browser refuses to run scripts that aren’t explicitly authorised. The
old policy carried 'unsafe-inline' for scripts, which is the CSP
equivalent of “authorise everything” and effectively disables the
protection. Removing it without breaking the UI required some care
because Astro pages legitimately emit small inline scripts (API base URL
injection, animation bootstrap, dashboard autoscroll).
The shipped solution:
- Every intentional inline script emits a placeholder:
<script is:inline nonce="d9197c1e80acbedbdaa9bfb16f1a030f">...</script> - nginx substitutes
d9197c1e80acbedbdaa9bfb16f1a030fwith an unpredictable per-request value ($request_id, 32 hex chars) on every HTML response. - The CSP header announces that same value:
script-src 'self' 'nonce-$request_id' https://challenges.cloudflare.com 'self'still covers Astro-hoisted external modules under/_astro/…. Inline scripts without a matching nonce are refused by the browser.
Curl it yourself: the header shows a different nonce on every reload,
and every <script> in the HTML carries the matching value.
CSP violation reporting
The new policy also points at a reporting endpoint:
report-uri https://api.sentrikat.com/api/security/csp-report
report-to csp-endpoint
Browsers POST a JSON payload there whenever a script is refused. We log those at warning level. The first week after deployment, CSP reports are the single most useful tool for catching any inline script we forgot to mark with a nonce.
SBOM, DPA, and the rest of the trust center
The audit pointed out — correctly — that our technical posture was hard
to see from the outside. A procurement officer going through a checklist
doesn’t git clone our repo; they open the website and look for
dedicated pages.
New pages live at:
/security— architecture, controls, responsible disclosure short form/compliance— GDPR, FADP, NIS2, DORA, ISO 27001 roadmap, CRA readiness/roadmap— dated commitments on ISO 27001 (Q4 2026), SOC 2 (Q1 2027), pen test (Q3 2026)/sbom— CycloneDX + SPDX, how we generate them, how to request archived ones/disclosure— full responsible disclosure policy with safe harbour/subprocessors— table of every third party we share data with/status— now with a 90-day persistent history, RSS feed, and email subscription
The /.well-known/security.txt now points at /disclosure as the canonical
policy.
Dependency integrity
Python requirements are moving to hash-pinned lockfiles via
pip-compile --generate-hashes. The source of truth is now
requirements.in; a single make deps-lock regenerates
requirements.txt with SHA-256 hashes per package. CI installs with
pip install --require-hashes so a compromised PyPI mirror cannot swap
a pinned version for a different artefact.
Less noise from the NVD monitor
Our data-source monitor probes upstream vulnerability APIs every six hours. NVD in particular has a long history of transient 503s. The previous logic alerted on any status flap; the new logic requires a status to persist across two consecutive probes (≥ 12 hours) before emailing the admin. A real outage still reaches us within a reasonable window; a single flaky response does not.
What’s not done yet
Transparency also means being honest about what’s queued:
style-src 'unsafe-inline'is still in place. Tailwind and Astro scoped styles produce thousands of inline<style>blocks; tightening this is a larger refactor.- The SaaS app at
app.sentrikat.com, the MkDocs docs site, and the Flarum community forum still carry'unsafe-inline'in their CSPs. The upstream templates don’t yet emit a nonce placeholder; we’re working on that. - ISO 27001 certification is in progress, target Q4 2026.
- SOC 2 Type I is planned for Q1 2027.
- Third-party pen test is scheduled for Q3 2026.
Everything above is also on /roadmap with updated dates whenever they change.
Reporting issues
If you find a security bug — even one not listed here — please email [email protected]. Our responsible disclosure policy and safe-harbor language are on /disclosure.
— Denis
Ready to automate your vulnerability management?
Deploy SentriKat on-premises in minutes. Track CISA KEV vulnerabilities, generate NIS2 compliance reports, and protect your infrastructure.
Request a Demo