-
Notifications
You must be signed in to change notification settings - Fork 0
Security
Pew Pew Collection is designed for a single trusted operator running on hardware they control. The security model is centered on protecting the local admin session, not on multi-tenant isolation.
-
Bcrypt password hashing at cost 12.
auth.service.jsusesbcrypt.compare/bcrypt.hash. The plainADMIN_PASSWORDenv var is only read on the very first boot to seed the hash. -
Forced password change on first login. The
must_change_passwordflag is set when the hash is seeded;requireAuthredirects to/change-passworduntil cleared. - Single admin user. No registration flow, no public signups, no API keys.
In production (NODE_ENV=production), the app refuses to start if:
-
ADMIN_PASSWORDis unset or equal tochangeme, and - there is no existing password hash in
settings.
This prevents the default credential from ever shipping live. Once the admin
hash exists in the database, subsequent restarts no longer need
ADMIN_PASSWORD set.
The app refuses to start in production if SESSION_SECRET is unset or equals
the documented default (ppcollection_dev_secret). Generate one with:
openssl rand -hex 32-
csrf-csrfdouble-submit cookie pattern. Token is set in a cookie and surfaced to templates asres.locals.csrfToken. - Every state-changing form embeds the token in a hidden input; rejected
requests render
errors/403.ejs.
express-rate-limit is applied on:
| Endpoint | Limit |
|---|---|
POST /login (failed only) |
10 per 15 min per IP |
POST /change-password |
20 per 15 min per IP |
Successful logins do not count against the limit.
| Flag | Value |
|---|---|
httpOnly |
always |
sameSite |
lax |
Secure |
true when NODE_ENV=production (or when SECURE_COOKIES=true) |
If you put the app behind nginx, Caddy, or Traefik with HTTPS:
| Your setup | What to set |
|---|---|
| HTTPS reverse proxy in front of the app | TRUST_PROXY=true |
| Plain HTTP in production (no TLS terminator) | SECURE_COOKIES=false |
Both NODE_ENV=production and a TLS proxy |
TRUST_PROXY=true and accept the default SECURE_COOKIES=true
|
Local dev at http://localhost:3000
|
Nothing — defaults are correct |
Without TRUST_PROXY=true, Express will see the proxied request as plain
HTTP, browsers will refuse to send the Secure cookie back, and sessions
will silently fail to persist.
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
}Pair with TRUST_PROXY=true on the app side.
-
helmetmiddleware with the default Content Security Policy enabled. -
method-overrideonly honors the_methodhidden form field — no query string overrides. - Request logging via
morgan; the/healthendpoint is excluded.
Login, logout, password change, and firearm create/update/delete/import events
are emitted as structured JSON on stdout. By default, usernames and serials
are redacted; set AUDIT_VERBOSE=true if you need them in the log.
Ship the container's stdout to your host log collector — journalctl, the
Docker json-file driver, Loki, etc.
firearms.validators.js enforces field length limits and numeric bounds
before anything reaches the repository layer. The repository layer is SQL only
and uses parameterized queries throughout. The serial column has a
database-level UNIQUE constraint, enforced for both form submission and CSV
import.
- Multi-user / role-based access control. Premium SaaS-style features are noted in the roadmap but not built into the core app.
- Network-level isolation. Run the app on a private network or behind a reverse proxy with auth in front if you want defense in depth.
- Encryption at rest. SQLite is plain on disk — encrypt the host volume if you need that.
See also: Configuration, Upgrading.
Pew Pew Collection · Self-hosted, offline-first firearm inventory · Business Source License 1.1
Getting started
Running it
Reference