Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion doc/admin/updates.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,9 @@ The notice auto-fades after 8 seconds and can be dismissed immediately. The publ

## Disabling everything

Set `updates.tier` to `"off"`. No HTTP request will leave the instance and no banner or badge will render.
Set `updates.tier` to `"off"`. The self-updater goes silent β€” no request to the GitHub Releases API leaves the instance and no banner or badge renders. Note this does **not** cover the separate legacy version check in `UpdateCheck.ts`, which still fetches `${updateServer}/info.json` until you also set `privacy.updateCheck` to `false` (see [PRIVACY.md](https://github.com/ether/etherpad/blob/develop/PRIVACY.md)).

On Docker / air-gapped installs you can do both without editing `settings.json` inside the image by setting `UPDATES_TIER=off` **and** `PRIVACY_UPDATE_CHECK=false` (add `PRIVACY_PLUGIN_CATALOG=false` to also disable the admin plugin browser's catalogue fetch). See the [Updates & privacy](../docker.md#updates--privacy-offline--air-gapped) table in the Docker docs for the full set of environment variables.

## Privacy

Expand Down
19 changes: 19 additions & 0 deletions doc/docker.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,25 @@ The `settings.json.docker` available by default allows to control almost every s
| `USER_PASSWORD` | the password for the first user `user` (leave unspecified if you do not want to create it) | |


### Updates & privacy (offline / air-gapped)

Etherpad makes a small number of outbound calls (a periodic version check and the admin plugin catalogue). In an air-gapped or firewalled deployment these can be disabled entirely without editing `settings.json` inside the image β€” set the variables below. See [PRIVACY.md](https://github.com/ether/etherpad/blob/develop/PRIVACY.md) and [doc/admin/updates.md](admin/updates.md) for what each call sends.

| Variable | Description | Default |
| --------------------------------- | ---------------------------------------------------------------------------------------------------------- | -------------------------------- |
| `PRIVACY_UPDATE_CHECK` | Set to `false` to disable the hourly version check (`UpdateCheck.ts`). | `true` |
| `PRIVACY_PLUGIN_CATALOG` | Set to `false` to disable the admin plugin browser (manual install-by-name via CLI still works). | `true` |
| `UPDATES_TIER` | Self-updater tier: `off` \| `notify` \| `manual` \| `auto` \| `autonomous`. Set to `off` to suppress the GitHub Releases check entirely. | `notify` |
| `UPDATES_SOURCE` | Where update metadata is fetched from. | `github` |
| `UPDATES_CHANNEL` | Release channel to track. | `stable` |
| `UPDATES_CHECK_INTERVAL_HOURS` | How often (hours) the updater polls when not `off`. | `6` |
| `UPDATES_GITHUB_REPO` | Repository the updater checks for releases. | `ether/etherpad` |
| `UPDATES_REQUIRE_ADMIN_FOR_STATUS`| Lock `/admin/update/status` to authenticated admins. | `false` |
| `UPDATE_SERVER` | Endpoint backing the version check. Point elsewhere (or disable the check above) for offline installs. | `https://etherpad.org/ep_infos` |

> **Fully offline:** set `UPDATES_TIER=off`, `PRIVACY_UPDATE_CHECK=false`, and `PRIVACY_PLUGIN_CATALOG=false`. The version check is fire-and-forget and already fails closed (a blocked endpoint is caught and logged, it does not prevent startup), but disabling it removes the outbound attempt and the log noise.


### Database

| Variable | Description | Default |
Expand Down
28 changes: 21 additions & 7 deletions settings.json.docker
Original file line number Diff line number Diff line change
Expand Up @@ -210,15 +210,17 @@
* tier: "off" | "notify" | "manual" | "auto" | "autonomous"
* Default "notify" shows a banner when an update is available.
* Docker installs are read-only β€” tiers above "notify" are not applied even if requested.
* Air-gapped / offline deployments should set UPDATES_TIER=off to suppress the
* periodic check against the GitHub Releases API entirely.
*/
"updates": {
"tier": "notify",
"source": "github",
"channel": "stable",
"tier": "${UPDATES_TIER:notify}",
"source": "${UPDATES_SOURCE:github}",
"channel": "${UPDATES_CHANNEL:stable}",
"installMethod": "docker",
"checkIntervalHours": 6,
"githubRepo": "ether/etherpad",
"requireAdminForStatus": false,
"checkIntervalHours": "${UPDATES_CHECK_INTERVAL_HOURS:6}",
"githubRepo": "${UPDATES_GITHUB_REPO:ether/etherpad}",
"requireAdminForStatus": "${UPDATES_REQUIRE_ADMIN_FOR_STATUS:false}",
"preApplyGraceMinutes": 0,
"drainSeconds": 60,
"rollbackHealthCheckSeconds": 60,
Expand Down Expand Up @@ -321,7 +323,19 @@
* https://etherpad.org/ep_infos
*/

"updateServer": "https://etherpad.org/ep_infos",
"updateServer": "${UPDATE_SERVER:https://etherpad.org/ep_infos}",

/*
* Outbound network calls. See PRIVACY.md for what each one sends.
* - PRIVACY_UPDATE_CHECK=false : disables the hourly version check (UpdateCheck.ts)
* - PRIVACY_PLUGIN_CATALOG=false : disables the admin plugin browser
* (manual install-by-name via CLI still works)
* Air-gapped / firewalled deployments should set both to false.
*/
"privacy": {
"updateCheck": "${PRIVACY_UPDATE_CHECK:true}",
"pluginCatalog": "${PRIVACY_PLUGIN_CATALOG:true}"
},

/*
* The type of the database.
Expand Down
10 changes: 6 additions & 4 deletions settings.json.template
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@
* Default "notify" shows a banner when an update is available. "off" disables the version check.
*/
"updates": {
"tier": "notify",
"tier": "${UPDATES_TIER:notify}",
"source": "github",
"channel": "stable",
"installMethod": "auto",
Expand Down Expand Up @@ -443,17 +443,19 @@
* https://etherpad.org/ep_infos
*/

"updateServer": "https://etherpad.org/ep_infos",
"updateServer": "${UPDATE_SERVER:https://etherpad.org/ep_infos}",

/*
* Outbound network calls. See PRIVACY.md for what each one sends.
* - updateCheck=false : disables hourly version check (UpdateCheck.ts)
* - pluginCatalog=false: disables admin plugin browser
* (manual install-by-name via CLI still works)
* Air-gapped / firewalled deployments should set both PRIVACY_UPDATE_CHECK and
* PRIVACY_PLUGIN_CATALOG to false (or UPDATES_TIER=off, which covers the version check).
*/
"privacy": {
"updateCheck": true,
"pluginCatalog": true
"updateCheck": "${PRIVACY_UPDATE_CHECK:true}",
"pluginCatalog": "${PRIVACY_PLUGIN_CATALOG:true}"
},

/*
Expand Down
81 changes: 81 additions & 0 deletions src/tests/backend/specs/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,4 +197,85 @@ describe(__filename, function () {
assert.strictEqual(settings.padOptions.fadeInactiveAuthorColors, true);
});
});

// Regression test for ether/etherpad#7911.
// Air-gapped / firewalled deployments must be able to disable Etherpad's
// outbound calls (version check + plugin catalogue + self-updater) purely
// via environment variables, without editing settings.json inside the image.
// These assertions parse the *shipped* settings.json.docker so a future edit
// that drops the ${ENV} placeholders fails loudly here.
describe('offline / air-gapped env overrides (issue #7911)', function () {
const dockerSettings = path.join(__dirname, '../../../../settings.json.docker');
const templateSettings = path.join(__dirname, '../../../../settings.json.template');
const envVars = [
'PRIVACY_UPDATE_CHECK', 'PRIVACY_PLUGIN_CATALOG', 'UPDATES_TIER',
'UPDATES_SOURCE', 'UPDATES_CHANNEL', 'UPDATES_CHECK_INTERVAL_HOURS',
'UPDATES_GITHUB_REPO', 'UPDATES_REQUIRE_ADMIN_FOR_STATUS', 'UPDATE_SERVER',
];
const saved: {[k: string]: string | undefined} = {};

before(function () { for (const v of envVars) saved[v] = process.env[v]; });
afterEach(function () {
for (const v of envVars) {
if (saved[v] == null) delete process.env[v];
else process.env[v] = saved[v];
}
});

it('keeps shipped defaults when no env vars are set', function () {
for (const v of envVars) delete process.env[v];
const s = exportedForTestingOnly.parseSettings(dockerSettings, true);
assert.strictEqual(s!.privacy.updateCheck, true);
assert.strictEqual(s!.privacy.pluginCatalog, true);
assert.strictEqual(s!.updates.tier, 'notify');
assert.strictEqual(s!.updates.checkIntervalHours, 6);
assert.strictEqual(s!.updateServer, 'https://etherpad.org/ep_infos');
});

it('disables all outbound calls when the offline env vars are set', function () {
process.env.PRIVACY_UPDATE_CHECK = 'false';
process.env.PRIVACY_PLUGIN_CATALOG = 'false';
process.env.UPDATES_TIER = 'off';
const s = exportedForTestingOnly.parseSettings(dockerSettings, true);
// Coerced to real booleans, not the strings "false".
assert.strictEqual(s!.privacy.updateCheck, false);
assert.strictEqual(s!.privacy.pluginCatalog, false);
assert.strictEqual(s!.updates.tier, 'off');
});

it('honours the remaining updates.* and updateServer overrides', function () {
process.env.UPDATES_SOURCE = 'gitlab';
process.env.UPDATES_CHANNEL = 'beta';
process.env.UPDATES_CHECK_INTERVAL_HOURS = '24';
process.env.UPDATES_GITHUB_REPO = 'acme/etherpad-fork';
process.env.UPDATES_REQUIRE_ADMIN_FOR_STATUS = 'true';
process.env.UPDATE_SERVER = 'https://mirror.internal/ep_infos';
const s = exportedForTestingOnly.parseSettings(dockerSettings, true);
assert.strictEqual(s!.updates.source, 'gitlab');
assert.strictEqual(s!.updates.channel, 'beta');
assert.strictEqual(s!.updates.checkIntervalHours, 24); // numeric coercion
assert.strictEqual(s!.updates.githubRepo, 'acme/etherpad-fork');
assert.strictEqual(s!.updates.requireAdminForStatus, true); // boolean coercion
assert.strictEqual(s!.updateServer, 'https://mirror.internal/ep_infos');
});

// The source-install template carries the same placeholders so non-Docker
// deployments get the offline knobs too.
it('settings.json.template exposes the same offline overrides', function () {
for (const v of envVars) delete process.env[v];
const dflt = exportedForTestingOnly.parseSettings(templateSettings, true);
assert.strictEqual(dflt!.privacy.updateCheck, true);
assert.strictEqual(dflt!.privacy.pluginCatalog, true);
assert.strictEqual(dflt!.updates.tier, 'notify');
assert.strictEqual(dflt!.updateServer, 'https://etherpad.org/ep_infos');

process.env.UPDATES_TIER = 'off';
process.env.PRIVACY_UPDATE_CHECK = 'false';
process.env.PRIVACY_PLUGIN_CATALOG = 'false';
const over = exportedForTestingOnly.parseSettings(templateSettings, true);
assert.strictEqual(over!.updates.tier, 'off');
assert.strictEqual(over!.privacy.updateCheck, false);
assert.strictEqual(over!.privacy.pluginCatalog, false);
});
});
});
Loading