Uni-Verein 1.8.1 β HTTPS Included and Comprehensive Security Hardening ππ‘οΈ
Β· 5 min read
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.ymlplusCaddyfileputs 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
admin123now 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.ymlandCaddyfileset up automatic HTTPS (requires a DNS record plus ports 80 and 443). The self-update flow detects and supports both setups and also updates theCaddyfile. nginx recovers the real client IP behind Caddy, so rate limiting and audit logs keep working - π¦ Login rate limiter (UV-31):
/auth/loginis limited per IP address (default 20 requests per window, configurable viaRateLimiting:AuthLogin:PermitLimit) - π Forced password change (UV-25): Logging in with the default password
admin123opens a dialog that can't be dismissed until a new password is set - π Automated dependency checks (UV-42):
npm audit --audit-level=highruns 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: nosniffandContent-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, andPermissions-Policy - π CORS (UV-28): Allowed origins are configured via
Cors:AllowedOrigins(empty in production) instead of allowing anylocalhostorigin - π Safe confirmation links (UV-35): Self-enrollment links use a stored
PublicBaseUrlinstead of the request'sHostheader, preventing phishing links via forged hosts - π¦ Containers without root (UV-37): All containers run as non-root users, with
cap_drop: [ALL]andno-new-privilegeswhere possible - ποΈ Secrets (UV-38, UV-39): The init script generates random database passwords and only prints the names of the
.envkeys, not their values..env.exampleships placeholders only instead of real keys - π§° Dependencies (UV-40):
Serilog.Sinks.Fileis pinned to the stable7.0.0, the unusedBCrypt.Net-Nextwas 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-IPis 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 port8080inside the container (the host port stays80), songinx.confand the compose file have to be replaced together. The update script does this for you logs/andreceipts/must be writable by the non-root backend (UID1654). The update script handles this viadocker-compose-ini.yml. On a manual setup, runchown -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!