Security Settings
The Security tab controls password rules, session lifetime, login lockout, and optional second-factor verification for admin accounts.
These settings apply to admin logins. Gallery passwords (the per-event password guests enter) are separate and have their own minimum-length rule (6 characters, no all-numeric).
Password rules
| Setting | Default | What it does |
|---|---|---|
| Minimum length | 8 | Enforced at admin creation, password change, and password reset. |
| Complexity | strong | One of simple, moderate, strong, very_strong. Each level adds requirements (uppercase, digit, symbol, no dictionary words). |
Validation runs server-side in backend/src/utils/passwordValidation.js. Failed validations return a field-level error with the specific rule that failed.
Session
| Setting | Default | What it does |
|---|---|---|
| Session timeout (minutes) | 60 | Admin is logged out after this many minutes of inactivity. Each request from the admin UI resets the timer. |
Token refresh: while the user is active the JWT is silently rotated; the timeout above is the idle limit, not the absolute one.
Lockout
PicPeak counts failed login attempts per (IP, email) pair and locks the pair out after the threshold:
| Setting | Default | What it does |
|---|---|---|
| Max login attempts | 5 | Failures before lockout triggers. |
| Attempt window (minutes) | 15 | Sliding window over which failures are counted. Older failures fall out. |
| Lockout duration (minutes) | 30 | How long a locked-out pair stays blocked. |
Lockouts log to auth_attempts and security_events. An admin can clear a lockout from the Users page.
Two-factor authentication
| Setting | Default | What it does |
|---|---|---|
| Enable 2FA | off | When on, admins must enrol a TOTP authenticator (Google Authenticator, 1Password, etc.) on their next login and supply a code on every subsequent login. |
Recovery codes are issued at enrolment — store them somewhere safe. There is no admin-side bypass for a locked-out 2FA user; you must reset the user’s password (which invalidates 2FA) from another admin account.
reCAPTCHA
Optional. When enabled, Google reCAPTCHA v3 is rendered on the admin login form (and any public registration form, if you have that on).
| Setting | Default | What it does |
|---|---|---|
| Enable reCAPTCHA | off | Master toggle. |
| Site key | (empty) | Public key from Google reCAPTCHA admin console — embedded into the page. |
| Secret key | (empty, masked in API) | Private key — sent to Google for verification on every login. |
The score threshold is hard-coded at 0.5 in the backend; below that the login is rejected with a generic “could not verify” error.
Gallery passwords
Gallery passwords and client PINs are stored as bcrypt hashes, so PicPeak cannot show you a password later. When a client loses it, the only way out has been Reset gallery password, which invalidates what the client already has.
| Setting | Default | What it does |
|---|---|---|
| Keep gallery passwords recoverable | off | Stores an encrypted copy of each gallery password and client PIN next to the hash. |
While it is on:
- Create, Publish, Edit, Reset gallery password and the v1 API keep the copy in sync with the hash. Turning a gallery’s password requirement off deletes its copy.
- The event page shows Show password with copy buttons for the gallery password and, if client access is on, the client PIN. Every reveal is written to the activity log as
gallery_password_viewed. - Resend creation email sends the stored password instead of the “set at creation” placeholder, so the client receives what already works.
Switching it off deletes every stored copy. Only passwords set while it is on are kept, so an existing gallery shows “No stored password” until its password is reset once.
The copy is AES-256-GCM encrypted with a key derived from GALLERY_PASSWORD_ENCRYPTION_KEY, or from JWT_SECRET when that variable is not set. Anyone with the database and the server configuration, or with admin access to the event, can read the passwords. Changing the key makes the stored copies unreadable; switch the setting off and on again to start over. Login and hash verification never touch the copy.
This is available on the beta channel from the build that ships PicPeak/picpeak#1271’s fix. On earlier builds the sent-mail archive under Messages still shows gallery passwords in clear text; from the same build on they are masked there.
API rate limiting
Independent of the login rules above, PicPeak limits how many API requests a single client IP may make. It exists to blunt scraping and credential stuffing, and it is worth knowing about because when it triggers it does not look like a limit: an affected visitor sees a gallery that stops loading, with no error anywhere they can see.
The API rate limiting card on this tab shows the values in force — the defaults below apply until you save something else — and saves with the tab’s Save Security Settings button. Beta builds from PicPeak/picpeak#1338 onward have the card; older builds have only the API described under Changing the limits without the card.
| Setting | Default | What it does |
|---|---|---|
| Enable the API rate limiter | on | Master toggle. |
| Window (minutes) | 15 | Sliding window per client IP. 1–60. |
| Max requests per window | 300 | Budget for everything under /api/ that is not exempt (see below). Exceeding it returns 429 Too Many Requests with a Retry-After header for the rest of the window. 10–10000. |
| Failed logins per window | 5 | Separate, much smaller budget for admin login and gallery password verification. Only failed attempts count. 1–100. |
| Do not count authenticated requests | on | Requests carrying a valid admin session do not count. Since v3.127.0-beta.0, a verified gallery viewer’s own image requests (thumbnail, preview, hero, photo) do not count either. |
| Count public endpoints only | off | When on, only /api/public/* and /api/gallery/* are counted. |
Saving applies at once: the limiter’s settings cache is cleared and the limiters are rebuilt with the new window, which also resets the current counters.
The unit is the IP address — not the person, and not the gallery. A photographer’s mental model is “one client, one gallery”; the budget applies to neither. Several people often share one address: a household, an office, a venue’s Wi-Fi and a mobile carrier’s NAT all present as a single IP, so a team of four reviewing a gallery from the same office draws on one budget of 300. Before v3.127.0-beta.0 every thumbnail counted, and a 546-photo grid exhausted the budget for a single viewer mid-scroll — the tiles that came back 429 rendered blank, and a refresh hit the limit again with the photo list. Since that version image requests of a verified viewer are exempt; a full gallery session then costs a few dozen counted requests, not hundreds.
What counts and what does not
Counted: every request to /api/ from a client without an admin session, including the gallery photo list, gallery info, password verification, feedback and downloads. Not counted: /health, the transfer upload endpoints (which have their own per-minute limits), admin sessions, and — from v3.127.0-beta.0 — image requests carrying a valid gallery token for that gallery.
These are the general limits. The per-image limits on the Image Security tab (requests per minute / 5 minutes / hour) are a separate mechanism for the secure-image routes and feed the suspicious-activity counter.
Behind a reverse proxy
The limiter keys on the client IP Express reports. Behind a proxy that is only correct when the proxy forwards X-Forwarded-For and PicPeak trusts that proxy. The default TRUST_PROXY (loopback, linklocal, uniquelocal) covers Docker networks and private-network proxies; a proxy with a public address needs an explicit TRUST_PROXY entry, otherwise every visitor shares the proxy’s single budget. See Reverse Proxy → Client IP.
Recognising a hit
The backend logs every rejected request as a warning:
Rate limit exceeded ip=… path=/api/gallery/<slug>/thumbnail/123 method=GETdocker compose logs backend | grep -c "Rate limit exceeded"A non-zero count while a visitor reports missing images is the limiter. On builds before v3.127.0-beta.0 the browser shows nothing: a 429 is a completed request, not a failed one, so it does not appear in the console, and the gallery rendered the missing tile as an empty box.
Changing the limits without the card
On builds before the card existed, the same six settings can be written through the admin API:
curl -X PUT "$PICPEAK_URL/api/admin/settings/security/rate-limit" \
-H "Authorization: Bearer $ADMIN_TOKEN" -H "Content-Type: application/json" \
-d '{"rate_limit_enabled":true,"rate_limit_window_minutes":15,"rate_limit_max_requests":1000,
"rate_limit_auth_max_requests":5,"rate_limit_skip_authenticated":true,"rate_limit_public_endpoints_only":false}'All six fields are required by that route. On builds before PicPeak/picpeak#1338 a changed window only takes effect after a backend restart (the other values apply within a minute); from that build on the route rebuilds the limiters and everything applies at once. If you would rather edit the database, the keys are the same six names in app_settings, JSON-encoded values.
Where the cache lives
Login middleware caches the security config in process memory for performance. Saving in this tab calls resetSecurityConfigCache() so changes take effect on the next request without a restart.