Skip to content

Commit 0136b13

Browse files
authored
release(v3.9.0): persistent-token key lifecycle updates and admin rotation workflow
- docker(startup): remove baked persistent-token key defaults and auto-generate a unique key for pristine installs - admin(ui): warn when the instance is still using a legacy or placeholder persistent-token key and expose guided rotation for compatible installs - admin(crypto): add persistent-token key rotation that re-encrypts stored secrets and expires remember-me sessions - docs(docker): refresh docker run / compose guidance so metadata-backed generated keys are documented as the default path
1 parent da9cbee commit 0136b13

11 files changed

Lines changed: 822 additions & 21 deletions

File tree

CHANGELOG.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,48 @@
11
# Changelog
22

3+
## Changes 03/14/2026 (v3.9.0)
4+
5+
`release(v3.9.0): persistent-token key lifecycle updates and admin rotation workflow`
6+
7+
**Commit message**
8+
9+
```text
10+
release(v3.9.0): persistent-token key lifecycle updates and admin rotation workflow
11+
12+
- docker(startup): remove baked persistent-token key defaults and auto-generate a unique key for pristine installs
13+
- admin(ui): warn when the instance is still using a legacy or placeholder persistent-token key and expose guided rotation for compatible installs
14+
- admin(crypto): add persistent-token key rotation that re-encrypts stored secrets and expires remember-me sessions
15+
- docs(docker): refresh docker run / compose guidance so metadata-backed generated keys are documented as the default path
16+
```
17+
18+
**Added**
19+
20+
- **Admin rotation workflow for persistent-token keys**
21+
- Added an admin-only rotation action that generates a new persistent-token key, re-encrypts stored secret-bearing data, writes `metadata/persistent_tokens.key`, and intentionally expires remember-me sessions.
22+
- Added an admin warning card with rotation guidance for instances still using a legacy or placeholder persistent-token key.
23+
24+
**Changed**
25+
26+
- **Docker startup behavior**
27+
- Pristine Docker installs now auto-generate and persist a unique persistent-token key in `metadata/persistent_tokens.key`.
28+
- Existing installs without an explicit key continue on the legacy compatibility path until the operator rotates them.
29+
- **Docker examples and env reference**
30+
- Updated `docker run`, compose, and env-reference guidance so `PERSISTENT_TOKENS_KEY` is optional by default and no published placeholder value is documented.
31+
32+
**Fixed**
33+
34+
- **Persistent-token key lifecycle**
35+
- Existing installs can now move off the legacy compatibility key without losing admin config, user-permissions, stored TOTP secrets, or source credentials.
36+
- Remember-me sessions are explicitly expired during rotation instead of being left in a mixed-key state.
37+
38+
**Security**
39+
40+
- **Install defaults**
41+
- The runtime image no longer ships a baked-in persistent-token key default.
42+
- New Docker installs now start with instance-unique key material by default as long as `metadata/` is persistent.
43+
44+
---
45+
346
## Changes 03/12/2026 (v3.8.0)
447

548
`release(v3.8.0): share-link admin guards and centralized safe-upload policy`

Dockerfile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,12 @@ RUN composer install --no-dev --optimize-autoloader # production-ready autoload
2525
FROM ubuntu:24.04
2626
LABEL by=error311
2727

28+
# No baked-in PERSISTENT_TOKENS_KEY default is shipped in the image.
29+
# Pristine installs generate and persist one at runtime in /var/www/metadata.
2830
ENV DEBIAN_FRONTEND=noninteractive \
2931
HOME=/root \
3032
LC_ALL=C.UTF-8 LANG=en_US.UTF-8 LANGUAGE=en_US.UTF-8 TERM=xterm \
3133
UPLOAD_MAX_FILESIZE=5G POST_MAX_SIZE=5G TOTAL_UPLOAD_SIZE=5G \
32-
PERSISTENT_TOKENS_KEY=default_please_change_this_key \
3334
PUID=99 PGID=100
3435

3536
ARG INSTALL_FFMPEG=0

README.md

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -162,14 +162,21 @@ The easiest way to run FileRise is the official Docker image.
162162
163163
### Option A – Quick start (docker run)
164164

165+
Pristine Docker installs can omit `PERSISTENT_TOKENS_KEY`. FileRise will generate a unique key on first start and persist it in `metadata/persistent_tokens.key`.
166+
167+
If you prefer to manage the key yourself, set one before first start:
168+
169+
```bash
170+
export PERSISTENT_TOKENS_KEY="$(openssl rand -hex 32)"
171+
```
172+
165173
```bash
166174
docker run -d \
167175
--name filerise \
168176
-p 8080:80 \
169177
-e TIMEZONE="America/New_York" \
170178
-e TOTAL_UPLOAD_SIZE="10G" \
171179
-e SECURE="false" \
172-
-e PERSISTENT_TOKENS_KEY="default_please_change_this_key" \
173180
-e SCAN_ON_START="true" \
174181
-e CHOWN_ON_START="true" \
175182
-v ~/filerise/uploads:/var/www/uploads \
@@ -194,6 +201,13 @@ On first launch you’ll be guided through creating the **initial admin user**.
194201
> (for example `~/filerise/uploads` or `/mnt/user/appdata/FileRise/uploads`),
195202
> not the root of a huge media share.
196203
>
204+
> 🔐 **Persistent tokens key note**
205+
>
206+
> Keep `/var/www/metadata` persistent. On a pristine install, FileRise writes the
207+
> generated persistent tokens key to `metadata/persistent_tokens.key`. If you
208+
> choose to manage the key via env instead, keep that value stable for the life
209+
> of the instance.
210+
>
197211
> If you really want FileRise to sit “on top of” an existing share, use a
198212
> subfolder (e.g. `/mnt/user/media/filerise_root`) instead of the share root,
199213
> so scans and permission changes stay scoped to that folder.
@@ -211,7 +225,7 @@ services:
211225
TIMEZONE: "America/New_York"
212226
TOTAL_UPLOAD_SIZE: "10G"
213227
SECURE: "false"
214-
PERSISTENT_TOKENS_KEY: "default_please_change_this_key"
228+
PERSISTENT_TOKENS_KEY: "${PERSISTENT_TOKENS_KEY:-}" # optional; blank = pristine installs auto-generate and persist a key in metadata
215229
SCAN_ON_START: "true" # auto-index existing files on startup
216230
CHOWN_ON_START: "true" # fix permissions on uploads/metadata on startup
217231
volumes:
@@ -226,14 +240,20 @@ Bring it up with:
226240
docker compose up -d
227241
```
228242

243+
You can leave `PERSISTENT_TOKENS_KEY` blank for pristine installs, or set it in your shell / `.env` if you want to manage the key yourself:
244+
245+
```bash
246+
export PERSISTENT_TOKENS_KEY="$(openssl rand -hex 32)"
247+
```
248+
229249
### Common environment variables
230250

231251
| Variable | Required | Example | What it does |
232252
|-------------------------|----------|----------------------------------|--------------|
233253
| `TIMEZONE` || `America/New_York` | PHP / container timezone. |
234254
| `TOTAL_UPLOAD_SIZE` || `10G` | Max total upload size per request; also used to set PHP/Apache upload limits. |
235255
| `SECURE` || `false` | `true` when running behind HTTPS / a reverse proxy, else `false`. |
236-
| `PERSISTENT_TOKENS_KEY` | | `change_me_super_secret` | Secret used to encrypt stored secrets (tokens, permissions, admin config). **Do not leave this at the default.** |
256+
| `PERSISTENT_TOKENS_KEY` | Optional | `openssl rand -hex 32` | Secret used to encrypt stored secrets (tokens, permissions, admin config). If omitted on a pristine Docker install, FileRise auto-generates and persists one in `metadata/persistent_tokens.key`; existing installs without an explicit key stay on the legacy compatibility path until rotated. |
237257
| `SCAN_ON_START` | Optional | `true` | If `true`, runs a scan once on container start to index existing files. |
238258
| `CHOWN_ON_START` | Optional | `true` | If `true`, recursively normalizes ownership/permissions on `uploads/` + `metadata/`. |
239259
| `PUID` | Optional | `99` | If running as root, remap `www-data` user to this UID (e.g. Unraid’s 99). |
@@ -324,11 +344,13 @@ Back up these paths (Docker volumes or host directories):
324344

325345
Notes:
326346
- Logs live in `/var/www/metadata/log` and can be rotated or pruned.
327-
- Keep your `PERSISTENT_TOKENS_KEY` consistent when restoring backups.
347+
- If you use the auto-generated key path, back up `/var/www/metadata/persistent_tokens.key` with the rest of `metadata/`.
348+
- If you manage the key via env, keep your `PERSISTENT_TOKENS_KEY` consistent when restoring backups.
328349

329350
## First-run security checklist
330351

331-
- Set a strong `PERSISTENT_TOKENS_KEY` (encrypts tokens, permissions, admin config).
352+
- Persist `/var/www/metadata` so the generated persistent tokens key survives container recreation.
353+
- Either set your own strong `PERSISTENT_TOKENS_KEY` or let a pristine install generate one and then back up `metadata/persistent_tokens.key`.
332354
- Use HTTPS and set `SECURE="true"` when behind TLS/reverse proxy.
333355
- If behind a proxy, set `FR_TRUSTED_PROXIES` and `FR_IP_HEADER`.
334356
- Set `FR_PUBLISHED_URL` (and `FR_BASE_PATH` if needed) so share links are correct.

config/config.php

Lines changed: 96 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
define('TRASH_DIR', UPLOAD_DIR . 'trash/');
4040
define('TIMEZONE', 'America/New_York');
4141
define('DATE_TIME_FORMAT','m/d/y h:iA');
42-
define('TOTAL_UPLOAD_SIZE','5G');
42+
define('TOTAL_UPLOAD_SIZE', '5G');
4343
define('REGEX_FOLDER_NAME','/^(?!^(?:CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$)(?!.*[. ]$)(?:[^<>:"\/\\\\|?*\x00-\x1F]{1,255})(?:[\/\\\\][^<>:"\/\\\\|?*\x00-\x1F]{1,255})*$/xu');
4444
define('PATTERN_FOLDER_NAME','[\p{L}\p{N}_\-\s\/\\\\]+');
4545
define('REGEX_FILE_NAME', '/^[^\x00-\x1F\/\\\\]{1,255}$/u');
@@ -191,14 +191,100 @@ function decryptData($encryptedData, $encryptionKey)
191191
return openssl_decrypt($ct, $cipher, $encryptionKey, OPENSSL_RAW_DATA, $iv);
192192
}
193193

194-
// Load encryption key
195-
$envKey = getenv('PERSISTENT_TOKENS_KEY');
196-
if ($envKey === false || $envKey === '') {
197-
$encryptionKey = 'default_please_change_this_key';
198-
error_log('WARNING: Using default encryption key. Please set PERSISTENT_TOKENS_KEY in your environment.');
199-
} else {
200-
$encryptionKey = $envKey;
194+
function fr_get_persistent_tokens_key_file_path(): string
195+
{
196+
return rtrim((string)META_DIR, "/\\") . DIRECTORY_SEPARATOR . 'persistent_tokens.key';
197+
}
198+
199+
function fr_resolve_persistent_tokens_key(): array
200+
{
201+
static $resolved = null;
202+
if (is_array($resolved)) {
203+
return $resolved;
204+
}
205+
206+
$defaultKey = 'default_please_change_this_key';
207+
$publishedPlaceholders = [$defaultKey, 'please_change_this_@@'];
208+
209+
$envKeyRaw = getenv('PERSISTENT_TOKENS_KEY');
210+
$envKey = trim($envKeyRaw === false ? '' : (string)$envKeyRaw);
211+
212+
$sourceHintRaw = getenv('PERSISTENT_TOKENS_KEY_SOURCE');
213+
$sourceHint = trim($sourceHintRaw === false ? '' : (string)$sourceHintRaw);
214+
215+
$keyFile = fr_get_persistent_tokens_key_file_path();
216+
$fileKey = '';
217+
if (is_file($keyFile)) {
218+
$raw = @file_get_contents($keyFile);
219+
if ($raw !== false) {
220+
$fileKey = trim((string)$raw);
221+
}
222+
}
223+
224+
$source = 'legacy_default';
225+
$key = $defaultKey;
226+
if ($envKey !== '') {
227+
$key = $envKey;
228+
if (in_array($sourceHint, ['env', 'file', 'generated_file', 'legacy_default'], true)) {
229+
$source = $sourceHint;
230+
} else {
231+
$source = 'env';
232+
}
233+
} elseif ($fileKey !== '') {
234+
$key = $fileKey;
235+
$source = 'file';
236+
}
237+
238+
$usesPublishedPlaceholder = in_array($key, $publishedPlaceholders, true);
239+
$usesLegacyDefault = ($source === 'legacy_default');
240+
$autoGenerated = ($source === 'generated_file');
241+
$needsAttention = $usesLegacyDefault || $usesPublishedPlaceholder;
242+
243+
$warning = '';
244+
$recommendedAction = '';
245+
if ($usesLegacyDefault) {
246+
$warning = 'FileRise is using the legacy built-in persistent tokens key because no explicit key is configured.';
247+
$recommendedAction = 'Set a unique key for new installs. For existing installs, plan a controlled rotation because changing the key can invalidate remember-me tokens and break decryption of stored secrets until they are re-encrypted.';
248+
} elseif ($usesPublishedPlaceholder) {
249+
$warning = 'FileRise is using a published placeholder persistent tokens key value.';
250+
$recommendedAction = 'Replace it with a unique key. For existing installs, rotate carefully because changing the key can invalidate remember-me tokens and break decryption of stored secrets until they are re-encrypted.';
251+
} elseif ($autoGenerated) {
252+
$recommendedAction = 'This Docker install auto-generated a key and stored it on disk. Back up metadata/persistent_tokens.key or set PERSISTENT_TOKENS_KEY explicitly before migrating the instance.';
253+
}
254+
255+
if ($needsAttention) {
256+
error_log('WARNING: ' . $warning);
257+
}
258+
259+
$resolved = [
260+
'key' => $key,
261+
'source' => $source,
262+
'usesPublishedPlaceholder' => $usesPublishedPlaceholder,
263+
'usesLegacyDefault' => $usesLegacyDefault,
264+
'autoGenerated' => $autoGenerated,
265+
'needsAttention' => $needsAttention,
266+
'warning' => $warning,
267+
'recommendedAction' => $recommendedAction,
268+
'keyFilePresent' => ($fileKey !== ''),
269+
];
270+
271+
return $resolved;
272+
}
273+
274+
function fr_load_persistent_tokens_key(): string
275+
{
276+
$resolved = fr_resolve_persistent_tokens_key();
277+
$key = (string)($resolved['key'] ?? '');
278+
return $key;
279+
}
280+
281+
function fr_get_persistent_tokens_key_status(): array
282+
{
283+
$resolved = fr_resolve_persistent_tokens_key();
284+
unset($resolved['key']);
285+
return $resolved;
201286
}
287+
$encryptionKey = fr_load_persistent_tokens_key();
202288
// Ensure encryption key is always available via $GLOBALS, even when this file
203289
// is required from function scope (e.g. API helper bootstrap wrappers).
204290
$GLOBALS['encryptionKey'] = $encryptionKey;
@@ -342,7 +428,8 @@ function loadUserPermissions($username)
342428
if (file_exists($adminConfigFile)) {
343429
$encrypted = file_get_contents($adminConfigFile);
344430
$decrypted = decryptData($encrypted, $encryptionKey);
345-
$adminCfg = json_decode($decrypted, true) ?: [];
431+
$json = ($decrypted !== false) ? $decrypted : $encrypted;
432+
$adminCfg = is_string($json) ? (json_decode($json, true) ?: []) : [];
346433

347434
$loginOpts = $adminCfg['loginOptions'] ?? [];
348435

docker-compose.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ services:
2323
DATE_TIME_FORMAT: "${DATE_TIME_FORMAT:-m/d/y h:iA}"
2424
TOTAL_UPLOAD_SIZE: "${TOTAL_UPLOAD_SIZE:-5G}"
2525
SECURE: "${SECURE:-false}"
26-
PERSISTENT_TOKENS_KEY: "${PERSISTENT_TOKENS_KEY:-please_change_this_@@}"
26+
# Optional: leave blank for pristine installs to auto-generate and persist
27+
# a unique key in ./data/metadata/persistent_tokens.key.
28+
PERSISTENT_TOKENS_KEY: "${PERSISTENT_TOKENS_KEY:-}"
2729
PUID: "${PUID:-1000}"
2830
PGID: "${PGID:-1000}"
2931
CHOWN_ON_START: "${CHOWN_ON_START:-true}"

docs/wiki/Environment-Variables-Full-Reference.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ This page lists all known environment variables read by FileRise core and the Do
44

55
Notes:
66
- Defaults reflect current code. Your deployment may override them.
7-
- Docker sets some defaults via the image (see `Dockerfile`/`start.sh`).
7+
- Docker sets some defaults via the image and startup script (see `Dockerfile` / `start.sh`). `PERSISTENT_TOKENS_KEY` can be omitted for pristine Docker installs, in which case FileRise auto-generates and persists one in `metadata/persistent_tokens.key`.
88

99
---
1010

@@ -16,7 +16,7 @@ Notes:
1616
| `DATE_TIME_FORMAT` | `m/d/y h:iA` | UI date/time format. |
1717
| `TOTAL_UPLOAD_SIZE` | `5G` | Max total upload size per request; used to set PHP upload limits and Apache `LimitRequestBody` in Docker. |
1818
| `SECURE` | auto | Set `true` when behind HTTPS to generate secure cookies/links. |
19-
| `PERSISTENT_TOKENS_KEY` | `default_please_change_this_key` | Encrypts stored secrets (tokens, permissions, admin config). Always change in production. |
19+
| `PERSISTENT_TOKENS_KEY` | empty | Encrypts stored secrets (tokens, permissions, admin config). Pristine Docker installs auto-generate one in `metadata/persistent_tokens.key`; back up that file or set a managed env key before migrating the instance. Legacy installs without an explicit key continue using the historical fallback until rotated deliberately. |
2020
| `FR_IGNORE_REGEX` | empty | Newline-separated regex patterns to ignore entries in listings/indexing; env overrides admin config. |
2121

2222
---
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
declare(strict_types=1);
3+
/**
4+
* @OA\Post(
5+
* path="/api/admin/rotatePersistentTokensKey.php",
6+
* summary="Rotate persistent tokens key",
7+
* description="Generates a new persistent tokens key, re-encrypts stored secrets, and expires remember-me sessions. Requires an authenticated admin session and CSRF token.",
8+
* operationId="adminRotatePersistentTokensKey",
9+
* tags={"Admin"},
10+
* security={{"cookieAuth": {}}},
11+
* @OA\Parameter(name="X-CSRF-Token", in="header", required=true, @OA\Schema(type="string")),
12+
* @OA\RequestBody(
13+
* required=true,
14+
* @OA\JsonContent(
15+
* required={"confirmRememberMeExpiry","confirmMaintenanceWindow"},
16+
* @OA\Property(property="confirmRememberMeExpiry", type="boolean", example=true),
17+
* @OA\Property(property="confirmMaintenanceWindow", type="boolean", example=true)
18+
* )
19+
* ),
20+
* @OA\Response(response=200, description="Rotation result"),
21+
* @OA\Response(response=400, description="Missing confirmation"),
22+
* @OA\Response(response=401, description="Unauthorized"),
23+
* @OA\Response(response=403, description="Forbidden"),
24+
* @OA\Response(response=409, description="Rotation not allowed in current deployment mode"),
25+
* @OA\Response(response=500, description="Server error")
26+
* )
27+
*/
28+
29+
require_once __DIR__ . '/../../../config/config.php';
30+
31+
$ctrl = new \FileRise\Http\Controllers\AdminController();
32+
$ctrl->rotatePersistentTokensKey();

0 commit comments

Comments
 (0)