Security Architecture
The headers, rate limits, authentication, and pentest results behind this site.
Security-First Approach
I built this site with the same security standards I apply to every project. This page documents what I've implemented and why - not to impress, but because I believe security decisions should be visible and reviewable.
Philosophy: Transparency over obscurity. Nothing here relies on an attacker not knowing how it works.
Security Check Status
Source available on requestAdvisories & Licences
Vulnerability Scan
Lint Checks
Test Suite
Commit: 80144b2
Produced by the same gate that runs before every push, on the machine that builds the site, not by a hosted CI service.
Content-Security-Policy (CSP)
default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; upgrade-insecure-requests; report-uri /api/csp-report; report-to csp-endpoint
Purpose: Controls which resources the browser is allowed to load
Prevents: Cross-Site Scripting (XSS), Data Injection, Clickjacking
X-Frame-Options
DENY
Purpose: Prevents the page from being embedded in frames/iframes
Prevents: Clickjacking attacks
X-Content-Type-Options
nosniff
Purpose: Prevents browsers from MIME-sniffing responses
Prevents: MIME confusion attacks, malicious file execution
Strict-Transport-Security (HSTS)
max-age=31536000; includeSubDomains; preload
Purpose: Forces all connections to use HTTPS for 1 year
Prevents: Man-in-the-Middle (MitM) attacks, SSL stripping
Additional Headers
- Reporting-Endpoints: Names the endpoint the CSP report-to directive sends violation reports to
- Referrer-Policy: Controls referrer information leakage
- Permissions-Policy: Disables 8 browser features (geolocation, camera, microphone, payment, USB, magnetometer, gyroscope, accelerometer)
- X-XSS-Protection: Legacy auditor disabled (CSP provides XSS protection)
- Cross-Origin-Opener-Policy: Isolates browsing context (Spectre protection)
- Cross-Origin-Resource-Policy: Prevents cross-origin resource embedding
- X-Permitted-Cross-Domain-Policies: Blocks Flash/Acrobat policy loading
- X-Request-ID: Unique UUID for audit trails
General Endpoints
Limit: 100 requests per 60 seconds per IP
Applies to: Every page, feed and API route. Static files such as CSS and images sit outside it.
Contact form: 3 messages, then 1 every 5 minutes per IP
Blocking: I can block single IPs and whole ranges from the admin. Blocks run in the app rather than the firewall, so a block affects this site only.
Authentication Endpoints
Limit: 5 requests per 60 seconds per IP
Purpose: Prevents brute force attacks
Implementation: actix-governor with RealIpKeyExtractor for accurate client identification
Password Security
- Algorithm: Argon2id (OWASP recommended)
- Why Argon2id: Memory-hard, resistant to GPU/ASIC attacks
- Policy: NIST 800-63B: length over complexity, and new passwords are checked against Have I Been Pwned. Only the first five characters of the password's SHA-1 hash leave the server.
- Hash cap: At most two password hashes run at once, so a flood of sign-in attempts can't exhaust the server's memory.
- Lockout: Five wrong passwords in a row from one IP lock that IP out of the account for 15 minutes. Other IPs are unaffected, so nobody can lock me out from elsewhere.
Session Management
- Session: One JWT access token, valid for 15 minutes. There are no refresh tokens: when it expires, I sign in again.
- Storage: The server sets it as an HttpOnly, SameSite=Strict cookie, Secure over HTTPS, so no script on the page can read it.
- CSRF: Requests from any other origin are refused, sibling subdomains included, and admin forms also carry a double-submit CSRF token.
- Logout: Revokes the token in Redis and clears the cookie.
Done on 3 October 2026. It read the middleware, sign-in, admin and logging code and the deploy files, and checked what the server shows from outside, without load or login attempts. The worst finding let about ten login requests at once stall the site, by running too many password hashes together. Two at a time is now the cap. Each finding is tracked to its fix. Two low ones are open: there is no outside uptime check yet, and one of the other apps on this server runs a process under this site's user ID.
The report is available on request via the contact form.
Run in February 2026 in eight phases: reconnaissance, TLS analysis, web application scanning, authentication testing, injection testing, configuration review, infrastructure scanning and exploitation attempts. The October audit above covers what changed since.
The full report is available on request via the contact form.
- Active PostgreSQL 17: Strong typing and ACID compliance
- Active Connection Pooling: Prevents connection exhaustion
- Active Prepared Statements: SQL injection prevention via SQLx
- Not used Row-Level Security: Not used. One database role owns and reads every table, so a per-row policy has nothing to express. It sat enabled without a policy until September 2026, a restore trap, and was turned off
Security Policy
- Dependabot: Automated dependency update alerts via GitHub
- Security Patches: Applied as soon as an advisory reaches the gate, which refuses every push until it is dealt with
- Pre-push gate: cargo audit, cargo deny, clippy and the full test suite run locally before any push is accepted
- Minimal Dependencies: Only essential, well-maintained crates
Server Infrastructure (Active)
- Host: Hetzner Cloud Helsinki (Debian 13)
- Service: systemd unit with automatic restart, running as its own user with a read-only system, no new privileges and no capabilities
- Proxy: nginx reverse proxy with TLS termination. The app itself listens on 127.0.0.1 only.
- Firewall: UFW - ports 22, 80, 443 only
- Neighbours: Two other apps of mine share this server, each under its own user. Neither has a login to this site's database.
Operations (Active)
- HTTPS: Let's Encrypt with auto-renewal. Since October 2026 the domain is on the HSTS preload list, so browsers that ship the list use HTTPS even on a first visit.
- Backups: Daily PostgreSQL dumps, plus an encrypted copy in Cloudflare R2 in the EU. The server can add to it but not delete from it for 30 days, and a restore from it was tested in October 2026.
- Monitoring: Health checks every 5 minutes, with alerts to my phone through ntfy
- Access: Dedicated deploy user with locked permissions
- Logs: Application logs are kept for 30 days
- Active Input Sanitisation: Submissions sanitised before database storage (stored-XSS prevention)
- Off PGP-Encrypted Email: Built but switched off since September 2026, because the server's outgoing mail port is blocked. When it is on, each message is encrypted to my PGP public key before it is sent. Today the database copy and a phone push that carries no sender details are the live paths.
- Active Delivery first: The database write comes first, so a failed push or email never blocks the form or loses a message
Disclosure Policy
- In scope: ericjingryd.com and everything it serves.
- Out of scope: The other hosts under this domain: admin, api, boka and ntfy, which belong to a booking app still in development, and cross, a private demo. If you notice a problem there, please report it, but don't test them.
- Not allowed: Denial of service, high-volume automated scanning, social engineering, and guessing passwords, including at the demo's sign-in prompt.
- Other people's data: Never access, change or keep it. If you reach some by accident, stop, tell me, and delete what you saw.
- Safe harbour: If you act in good faith and within these rules, I won't take legal action or report you, and I'll credit you if you'd like.
Security Contact & Responsible Disclosure
Responsible disclosure is welcome. Please read the policy above first.
I reply within 24 hours. Critical and high issues are fixed or mitigated within 48 hours, and anything else gets a plan and a date in my reply. Machine-readable contact details are at /.well-known/security.txt, and my PGP key is at /pgp-key.asc.