Skip to main content

Uni-Verein 1.8.1 – HTTPS Included and Comprehensive Security Hardening πŸ”πŸ›‘οΈ

Β· 5 min read
RenΓ© Herrmann
Senior developer

Published on September 26, 2026

Uni-Verein 1.8.1 is a security release: the installation gets HTTPS out of the box, the login becomes much more resistant to attacks, uploads are checked more strictly, and all containers now run without root privileges.

Highlights​

  • πŸ” HTTPS out of the box: A new docker-compose-tls.yml plus Caddyfile puts Caddy in front of the stack and automatically fetches a Let's Encrypt certificate. This is now the recommended installation path
  • πŸ›‘οΈ Hardened login: Per-IP rate limiting, no more hints about whether a username exists, and lockouts last at most 15 minutes instead of up to 24 hours
  • πŸ”‘ No more default admin password in use: Logging in with admin123 now forces you to set a new password first
  • πŸ“¦ Safer uploads and containers: Receipt files are checked by their actual content, upload sizes are capped, and every container runs as a non-root user

New Features​

  • πŸ” HTTPS with Let's Encrypt (UV-27): docker-compose-tls.yml and Caddyfile set up automatic HTTPS (requires a DNS record plus ports 80 and 443). The self-update flow detects and supports both setups and also updates the Caddyfile. nginx recovers the real client IP behind Caddy, so rate limiting and audit logs keep working
  • 🚦 Login rate limiter (UV-31): /auth/login is limited per IP address (default 20 requests per window, configurable via RateLimiting:AuthLogin:PermitLimit)
  • πŸ”‘ Forced password change (UV-25): Logging in with the default password admin123 opens a dialog that can't be dismissed until a new password is set
  • πŸ” Automated dependency checks (UV-42): npm audit --audit-level=high runs in CI, plus Dependabot (one bundled weekly PR, no major updates)

Improvements​

  • πŸ”’ Authentication by default (UV-41): Endpoints now require authentication by default. Intentionally public ones (login, self-enrollment, public page configuration, /health) are explicitly allowed
  • ⏱️ Fairer lockouts (UV-45): Failed login attempts expire after 15 minutes, and the lockout is capped at 15 minutes (previously up to 24 hours)
  • πŸ‘€ Admin protection (UV-25): The default admin account is only created when no users exist at all. Admins can no longer delete their own account or change their own role
  • 🧾 Uploads checked by content (UV-32): Receipt files are validated by their magic bytes (PNG, JPEG, GIF, WebP, PDF) instead of the type reported by the browser. SVG and unknown formats are rejected. Downloads send X-Content-Type-Options: nosniff and Content-Disposition: attachment
  • πŸ“ Upload limits (UV-43): The global request limit drops from 10 GB to 100 MB, only the backup restore keeps 10 GB. Receipt files and the CSV import of members are capped at 10 MB per file, and oversized PDFs are rejected right in the browser
  • 🧱 Security headers (UV-33): Both nginx configs send CSP, X-Frame-Options, nosniff, Referrer-Policy, and Permissions-Policy
  • 🌐 CORS (UV-28): Allowed origins are configured via Cors:AllowedOrigins (empty in production) instead of allowing any localhost origin
  • πŸ”— Safe confirmation links (UV-35): Self-enrollment links use a stored PublicBaseUrl instead of the request's Host header, preventing phishing links via forged hosts
  • πŸ“¦ Containers without root (UV-37): All containers run as non-root users, with cap_drop: [ALL] and no-new-privileges where possible
  • πŸ—οΈ Secrets (UV-38, UV-39): The init script generates random database passwords and only prints the names of the .env keys, not their values. .env.example ships placeholders only instead of real keys
  • 🧰 Dependencies (UV-40): Serilog.Sinks.File is pinned to the stable 7.0.0, the unused BCrypt.Net-Next was removed, plus minor/patch updates for backend and frontend

Fixes​

  • πŸ–₯️ Blank screen after login (UV-18): After an update, stale lazily-loaded parts of the app could cause an empty screen. The app now reloads automatically when a new service worker takes over
  • πŸ”” Firmware hint (UV-46): The firmware-update hint now compares real version numbers and only shows when a newer version actually exists
  • πŸ•΅οΈ Username enumeration (UV-44): Login response timing no longer reveals whether a username exists. Raw usernames are no longer written to the log
  • 🌍 Forged IP headers (UV-34): X-Real-IP is only trusted from private/internal networks, so IP-based rate limits can no longer be bypassed with a spoofed header

⚠️ Upgrade Notes​

  • Always update with docker-compose-update.yml, as described in the README. Proxy and frontend now listen on port 8080 inside the container (the host port stays 80), so nginx.conf and the compose file have to be replaced together. The update script does this for you
  • logs/ and receipts/ must be writable by the non-root backend (UID 1654). The update script handles this via docker-compose-ini.yml. On a manual setup, run chown -R 1654:1654 logs receipts
  • Self-enrollment users: If self-enrollment was already enabled before this update, an admin has to open the general settings and save them once. This stores the public URL used in confirmation links. Until then, new submissions fail
  • The database migration (AddLastFailedLoginAttemptAndPublicBaseUrl) runs automatically on startup

Why These Updates Matter​

A club management system holds names, addresses, and bank details, so it should be run as securely as possible. With HTTPS out of the box, a more robust login, and stricter checks for uploads and network access, 1.8.1 closes a whole series of possible attack surfaces at once. Running the containers without root privileges also limits the damage should something ever go wrong.

What's Next?​

Real-world feedback is, as always, very welcome via GitHub Issues. Check back here again soon!