mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
Merge pull request #1265 from bilawalsidhu/codex/fix-docker-trusted-peers
fix(docker): honor trusted-peer environment overrides
This commit is contained in:
9 files changed
+223
-6
No files matched your search
@@ -177,6 +177,16 @@ Publishing it wider is the [Sharing an instance](#-sharing-an-instance) opt-in,
|
|||||||
|
|
||||||
When you widen the publish, also set `GEV_KEY_SETUP_TRUSTED_PEERS=none` so the launcher trusts no gateway address.
|
When you widen the publish, also set `GEV_KEY_SETUP_TRUSTED_PEERS=none` so the launcher trusts no gateway address.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
GEV_KEY_SETUP_TRUSTED_PEERS=none docker compose up
|
||||||
|
```
|
||||||
|
|
||||||
|
Compose forwards `GEV_KEY_SETUP_TRUSTED_PEERS` and `GEV_ALLOWED_HOSTS` from your
|
||||||
|
shell or the project's `.env`; an exported shell value takes precedence. You can
|
||||||
|
also put `GEV_KEY_SETUP_TRUSTED_PEERS=none` in `.env` for a persistent setting.
|
||||||
|
Unset or blank trusted peers retain automatic gateway discovery; `none` explicitly
|
||||||
|
disables it. The startup log confirms that no additional peers are trusted.
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>Running on Linux, a NAS, or behind a reverse proxy</summary>
|
<summary>Running on Linux, a NAS, or behind a reverse proxy</summary>
|
||||||
|
|
||||||
|
|||||||
@@ -100,6 +100,11 @@ The dev server is a **key broker**: every server-side key above is spendable by
|
|||||||
`compose.yaml` publishes on `127.0.0.1`: **widen that mapping and every LAN
|
`compose.yaml` publishes on `127.0.0.1`: **widen that mapping and every LAN
|
||||||
client arrives from the same gateway address, which hands them Provider
|
client arrives from the same gateway address, which hands them Provider
|
||||||
Settings too.** Treat publishing beyond loopback as the LAN opt-in above.
|
Settings too.** Treat publishing beyond loopback as the LAN opt-in above.
|
||||||
|
Compose forwards `GEV_KEY_SETUP_TRUSTED_PEERS` and `GEV_ALLOWED_HOSTS` from
|
||||||
|
the shell or project `.env` before the launcher runs. Set trusted peers to
|
||||||
|
`none` when widening the publish: it becomes an explicit empty list before
|
||||||
|
Vite loads configuration, so the gateway cannot regain Provider Settings
|
||||||
|
access. Unset or blank values preserve automatic gateway discovery.
|
||||||
- **Provider-side budgets are the real backstop.** For hard spend protection, configure limits where the money is: OpenAI platform usage limits, Google Cloud budget alerts + per-API quotas, and equivalent controls for any other keyed provider.
|
- **Provider-side budgets are the real backstop.** For hard spend protection, configure limits where the money is: OpenAI platform usage limits, Google Cloud budget alerts + per-API quotas, and equivalent controls for any other keyed provider.
|
||||||
- **Pinokio LAN and Cloudflare sharing are refused.** The current supported
|
- **Pinokio LAN and Cloudflare sharing are refused.** The current supported
|
||||||
Pinokio release re-reads sharing state when an app registers its Open URL and
|
Pinokio release re-reads sharing state when an app registers its Open URL and
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
- Honor Docker Compose trusted-peer and allowed-host settings from the shell
|
||||||
|
or project `.env`, and let `GEV_KEY_SETUP_TRUSTED_PEERS=none` disable gateway
|
||||||
|
access to Provider Settings before Vite starts (asturksever, #1265).
|
||||||
@@ -5,6 +5,11 @@ services:
|
|||||||
# lands in the bind-mounted repo, so run as you, not root. UID/GID are
|
# lands in the bind-mounted repo, so run as you, not root. UID/GID are
|
||||||
# unset on Docker Desktop, where the 1000 fallback is ignored anyway.
|
# unset on Docker Desktop, where the 1000 fallback is ignored anyway.
|
||||||
user: "${UID:-1000}:${GID:-1000}"
|
user: "${UID:-1000}:${GID:-1000}"
|
||||||
|
# Forward explicit shell/.env policy before the launcher discovers a gateway.
|
||||||
|
# Unset values stay unset so the default loopback-only install still works.
|
||||||
|
environment:
|
||||||
|
- GEV_KEY_SETUP_TRUSTED_PEERS
|
||||||
|
- GEV_ALLOWED_HOSTS
|
||||||
ports:
|
ports:
|
||||||
# Publish to loopback only. Docker's default ("4173:4173") would bind
|
# Publish to loopback only. Docker's default ("4173:4173") would bind
|
||||||
# every host interface, which quietly turns the LAN warning in the README
|
# every host interface, which quietly turns the LAN warning in the README
|
||||||
|
|||||||
@@ -9,6 +9,15 @@ broader suffix variants. Follow-ups retain boundary provenance, and cancelled
|
|||||||
lookups preserve the previous analyst cohort and boundary source. Navigation
|
lookups preserve the previous analyst cohort and boundary source. Navigation
|
||||||
search keeps a successful geocode, with no search or view-based region recovery.
|
search keeps a successful geocode, with no search or view-based region recovery.
|
||||||
|
|
||||||
|
Docker Compose forwards `GEV_KEY_SETUP_TRUSTED_PEERS` and `GEV_ALLOWED_HOSTS`
|
||||||
|
from the shell or project `.env` before `scripts/docker-start.mjs` runs; shell
|
||||||
|
values take precedence. The launcher's `none` policy resolves to an empty trusted
|
||||||
|
peer list and is assigned before importing Vite, preventing later dotenv loading
|
||||||
|
from replacing it. Unset or blank values retain automatic gateway discovery.
|
||||||
|
Startup logs distinguish an explicit `none` policy from a missing default route.
|
||||||
|
This changes neither loopback-only port publishing nor the Provider Settings
|
||||||
|
Host, Origin, proxy-header and sharing checks.
|
||||||
|
|
||||||
Shared distance checks use `src/geoDistance.js` with `{ lat, lon }` points and
|
Shared distance checks use `src/geoDistance.js` with `{ lat, lon }` points and
|
||||||
the IUGG mean spherical Earth radius, 6371.0088 km. `greatCircleKm` is
|
the IUGG mean spherical Earth radius, 6371.0088 km. `greatCircleKm` is
|
||||||
canonical; `greatCircleMeters` is its exact metre conversion. Missing,
|
canonical; `greatCircleMeters` is its exact metre conversion. Missing,
|
||||||
|
|||||||
@@ -48,7 +48,8 @@ export function defaultGatewayFromRouteTable(text) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Decide this launch's trusted-peer list. An operator value always wins, so a
|
* Decide this launch's trusted-peer list. An operator value always wins, so a
|
||||||
* compose override stays authoritative; otherwise the gateway alone is
|
* compose override stays authoritative; `none` trusts no additional peers.
|
||||||
|
* An unset or blank value means the gateway alone is
|
||||||
* trusted. No gateway trusts nothing and Provider Settings stays loopback-only,
|
* trusted. No gateway trusts nothing and Provider Settings stays loopback-only,
|
||||||
* exactly as under `npm run dev`.
|
* exactly as under `npm run dev`.
|
||||||
* @param {{env?: NodeJS.ProcessEnv, routeTable?: string}} input
|
* @param {{env?: NodeJS.ProcessEnv, routeTable?: string}} input
|
||||||
@@ -56,6 +57,7 @@ export function defaultGatewayFromRouteTable(text) {
|
|||||||
*/
|
*/
|
||||||
export function resolveTrustedPeers({ env = process.env, routeTable = '' } = {}) {
|
export function resolveTrustedPeers({ env = process.env, routeTable = '' } = {}) {
|
||||||
const explicit = String(env.GEV_KEY_SETUP_TRUSTED_PEERS ?? '').trim();
|
const explicit = String(env.GEV_KEY_SETUP_TRUSTED_PEERS ?? '').trim();
|
||||||
|
if (explicit.toLowerCase() === 'none') return '';
|
||||||
if (explicit) return explicit;
|
if (explicit) return explicit;
|
||||||
return defaultGatewayFromRouteTable(routeTable) ?? '';
|
return defaultGatewayFromRouteTable(routeTable) ?? '';
|
||||||
}
|
}
|
||||||
@@ -109,15 +111,17 @@ async function start() {
|
|||||||
String(process.env.GEV_KEY_SETUP_TRUSTED_PEERS ?? '').trim(),
|
String(process.env.GEV_KEY_SETUP_TRUSTED_PEERS ?? '').trim(),
|
||||||
);
|
);
|
||||||
const trustedPeers = resolveTrustedPeers({ routeTable: readRouteTable() });
|
const trustedPeers = resolveTrustedPeers({ routeTable: readRouteTable() });
|
||||||
|
// Set even an empty list before Vite loads .env: the resolved launcher policy
|
||||||
|
// must remain authoritative when there is no trusted gateway.
|
||||||
|
process.env.GEV_KEY_SETUP_TRUSTED_PEERS = trustedPeers;
|
||||||
if (trustedPeers) {
|
if (trustedPeers) {
|
||||||
// Set before Vite is imported: the dev server snapshots process.env during
|
|
||||||
// configuration, and the admission gate reads it per request.
|
|
||||||
process.env.GEV_KEY_SETUP_TRUSTED_PEERS = trustedPeers;
|
|
||||||
console.log(
|
console.log(
|
||||||
fromOperator
|
fromOperator
|
||||||
? `[Docker] Provider Settings trusted peers: ${trustedPeers} (GEV_KEY_SETUP_TRUSTED_PEERS).`
|
? `[Docker] Provider Settings trusted peers: ${trustedPeers} (GEV_KEY_SETUP_TRUSTED_PEERS).`
|
||||||
: `[Docker] Provider Settings trusts the container gateway (${trustedPeers}).`,
|
: `[Docker] Provider Settings trusts the container gateway (${trustedPeers}).`,
|
||||||
);
|
);
|
||||||
|
} else if (fromOperator) {
|
||||||
|
console.log('[Docker] Provider Settings trusts no additional peers (GEV_KEY_SETUP_TRUSTED_PEERS=none).');
|
||||||
} else {
|
} else {
|
||||||
console.log('[Docker] No default gateway; Provider Settings stays loopback-only.');
|
console.log('[Docker] No default gateway; Provider Settings stays loopback-only.');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1087,5 +1087,6 @@
|
|||||||
"src/ui/styles/status-badge.css",
|
"src/ui/styles/status-badge.css",
|
||||||
"src/ui/statusBadge.test.mjs",
|
"src/ui/statusBadge.test.mjs",
|
||||||
"src/ui/cctvBadges.test.mjs",
|
"src/ui/cctvBadges.test.mjs",
|
||||||
"scripts/qa-status-badges.mjs"
|
"scripts/qa-status-badges.mjs",
|
||||||
|
"src/dockerTrustedPeers.test.mjs"
|
||||||
]
|
]
|
||||||
@@ -72,6 +72,7 @@ test('the Docker install path keeps its loopback-only, launcher-owned contract',
|
|||||||
|
|
||||||
const compose = readFileSync(new URL('../compose.yaml', import.meta.url), 'utf8');
|
const compose = readFileSync(new URL('../compose.yaml', import.meta.url), 'utf8');
|
||||||
assert.match(compose, /^\s+- "127\.0\.0\.1:4173:4173"$/m, 'the port is published on the host loopback only');
|
assert.match(compose, /^\s+- "127\.0\.0\.1:4173:4173"$/m, 'the port is published on the host loopback only');
|
||||||
assert.doesNotMatch(compose, /GEV_KEY_SETUP_TRUSTED_PEERS/, 'trust is derived at launch, never hard-coded');
|
assert.match(compose, /^\s+- GEV_KEY_SETUP_TRUSTED_PEERS$/m, 'the operator policy reaches the launcher without a hard-coded address');
|
||||||
|
assert.match(compose, /^\s+- GEV_ALLOWED_HOSTS$/m, 'the host allowlist reaches Vite');
|
||||||
assert.doesNotMatch(compose, /network_mode/, 'the container keeps its own network namespace');
|
assert.doesNotMatch(compose, /network_mode/, 'the container keeps its own network namespace');
|
||||||
});
|
});
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { spawnSync } from 'node:child_process';
|
||||||
|
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||||||
|
import { tmpdir } from 'node:os';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import test from 'node:test';
|
||||||
|
import { resolveTrustedPeers } from '../scripts/docker-start.mjs';
|
||||||
|
|
||||||
|
const ROUTES = 'Iface Destination Gateway Flags\neth0 00000000 0100A8C0 0003\n';
|
||||||
|
const launcher = fileURLToPath(
|
||||||
|
new URL('../scripts/docker-start.mjs', import.meta.url),
|
||||||
|
);
|
||||||
|
const gateUrl = new URL('./keySetupCore.mjs', import.meta.url).href;
|
||||||
|
|
||||||
|
test('none resolves to an empty trust list even with a valid container gateway', () => {
|
||||||
|
for (const value of ['none', ' NONE ', 'None']) {
|
||||||
|
assert.equal(
|
||||||
|
resolveTrustedPeers({
|
||||||
|
env: { GEV_KEY_SETUP_TRUSTED_PEERS: value },
|
||||||
|
routeTable: ROUTES,
|
||||||
|
}),
|
||||||
|
'',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function launchWithPolicy(value, routeTable = ROUTES) {
|
||||||
|
// Run the real entry point and routing-table reader. Replace only the Linux
|
||||||
|
// route file and Vite's listening server so this works on macOS and CI too.
|
||||||
|
const fakeVite = `
|
||||||
|
import { admitKeySetupRequest } from ${JSON.stringify(gateUrl)};
|
||||||
|
const trustedAtImport = process.env.GEV_KEY_SETUP_TRUSTED_PEERS;
|
||||||
|
export async function createServer() {
|
||||||
|
const request = {
|
||||||
|
method: 'POST', hostHeader: 'localhost:4173',
|
||||||
|
origin: 'http://localhost:4173', contentType: 'application/json',
|
||||||
|
env: { GEV_KEY_SETUP_TRUSTED_PEERS: process.env.GEV_KEY_SETUP_TRUSTED_PEERS }
|
||||||
|
};
|
||||||
|
console.log('POLICY ' + JSON.stringify({
|
||||||
|
trustedAtImport,
|
||||||
|
allowedHosts: process.env.GEV_ALLOWED_HOSTS,
|
||||||
|
gateway: admitKeySetupRequest({ ...request, remoteAddress: '192.168.0.1' }),
|
||||||
|
loopback: admitKeySetupRequest({ ...request, remoteAddress: '127.0.0.1' }),
|
||||||
|
foreignOrigin: admitKeySetupRequest({ ...request, remoteAddress: '192.168.0.1', origin: 'https://foreign.example' })
|
||||||
|
}));
|
||||||
|
return { async listen() {}, printUrls() {}, async close() {} };
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
const preload = `
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import { registerHooks, syncBuiltinESMExports } from 'node:module';
|
||||||
|
const originalRead = fs.readFileSync;
|
||||||
|
fs.readFileSync = (file, ...args) => file === '/proc/net/route'
|
||||||
|
? ${JSON.stringify(routeTable)} : originalRead(file, ...args);
|
||||||
|
syncBuiltinESMExports();
|
||||||
|
registerHooks({ resolve(specifier, context, next) {
|
||||||
|
if (specifier === 'vite') return {
|
||||||
|
url: ${JSON.stringify(`data:text/javascript,${encodeURIComponent(fakeVite)}`)}, shortCircuit: true
|
||||||
|
};
|
||||||
|
return next(specifier, context);
|
||||||
|
}});
|
||||||
|
`;
|
||||||
|
const env = { ...process.env, GEV_ALLOWED_HOSTS: 'globe.example.lan' };
|
||||||
|
delete env.GEV_KEY_SETUP_TRUSTED_PEERS;
|
||||||
|
if (value !== undefined) env.GEV_KEY_SETUP_TRUSTED_PEERS = value;
|
||||||
|
const child = spawnSync(
|
||||||
|
process.execPath,
|
||||||
|
[
|
||||||
|
'--import',
|
||||||
|
`data:text/javascript,${encodeURIComponent(preload)}`,
|
||||||
|
launcher,
|
||||||
|
],
|
||||||
|
{ env, encoding: 'utf8', timeout: 10000 },
|
||||||
|
);
|
||||||
|
assert.ifError(child.error);
|
||||||
|
assert.equal(child.status, 0, child.stderr);
|
||||||
|
const line = child.stdout
|
||||||
|
.split('\n')
|
||||||
|
.find((line) => line.startsWith('POLICY '));
|
||||||
|
assert.ok(line, child.stdout);
|
||||||
|
return { ...JSON.parse(line.slice(7)), output: child.stdout };
|
||||||
|
}
|
||||||
|
|
||||||
|
test('the real Docker entry point seals none before importing Vite and the setup gate refuses the gateway', () => {
|
||||||
|
const result = launchWithPolicy('none');
|
||||||
|
assert.equal(result.trustedAtImport, '');
|
||||||
|
assert.equal(result.gateway.status, 403);
|
||||||
|
assert.equal(result.gateway.code, 'not-loopback');
|
||||||
|
assert.equal(result.loopback.ok, true);
|
||||||
|
assert.equal(result.allowedHosts, 'globe.example.lan');
|
||||||
|
assert.match(result.output, /trusts no additional peers/);
|
||||||
|
assert.doesNotMatch(result.output, /No default gateway/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('unset, blank and explicit peers retain gateway admission without weakening Origin checks', () => {
|
||||||
|
for (const value of [undefined, '', ' ', '192.168.0.1']) {
|
||||||
|
const result = launchWithPolicy(value);
|
||||||
|
assert.equal(result.trustedAtImport, '192.168.0.1');
|
||||||
|
assert.equal(result.gateway.ok, true);
|
||||||
|
assert.equal(result.foreignOrigin.ok, false);
|
||||||
|
}
|
||||||
|
const result = launchWithPolicy(undefined, '');
|
||||||
|
assert.equal(result.trustedAtImport, '');
|
||||||
|
assert.equal(result.gateway.status, 403);
|
||||||
|
assert.match(result.output, /No default gateway/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Compose resolves shell and project dotenv policy before the launcher starts', (t) => {
|
||||||
|
const version = spawnSync('docker', ['compose', 'version'], {
|
||||||
|
encoding: 'utf8',
|
||||||
|
timeout: 10000,
|
||||||
|
});
|
||||||
|
if (version.error || version.status !== 0)
|
||||||
|
return t.skip('Docker Compose CLI is not installed');
|
||||||
|
const root = mkdtempSync(path.join(tmpdir(), 'gev-compose-policy-'));
|
||||||
|
t.after(() => rmSync(root, { recursive: true, force: true }));
|
||||||
|
writeFileSync(
|
||||||
|
path.join(root, 'compose.yaml'),
|
||||||
|
readFileSync(new URL('../compose.yaml', import.meta.url)),
|
||||||
|
);
|
||||||
|
const baseEnv = { ...process.env };
|
||||||
|
for (const key of Object.keys(baseEnv)) {
|
||||||
|
if (
|
||||||
|
key.startsWith('COMPOSE_') ||
|
||||||
|
['GEV_KEY_SETUP_TRUSTED_PEERS', 'GEV_ALLOWED_HOSTS'].includes(key)
|
||||||
|
)
|
||||||
|
delete baseEnv[key];
|
||||||
|
}
|
||||||
|
function config(dotenv = '', overrides = {}) {
|
||||||
|
writeFileSync(path.join(root, '.env'), dotenv);
|
||||||
|
const child = spawnSync(
|
||||||
|
'docker',
|
||||||
|
[
|
||||||
|
'compose',
|
||||||
|
'-f',
|
||||||
|
'compose.yaml',
|
||||||
|
'--env-file',
|
||||||
|
'.env',
|
||||||
|
'config',
|
||||||
|
'--format',
|
||||||
|
'json',
|
||||||
|
],
|
||||||
|
{
|
||||||
|
cwd: root,
|
||||||
|
env: { ...baseEnv, ...overrides },
|
||||||
|
encoding: 'utf8',
|
||||||
|
timeout: 10000,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assert.ifError(child.error);
|
||||||
|
assert.equal(child.status, 0, child.stderr);
|
||||||
|
return JSON.parse(child.stdout).services['gods-eye-view'].environment || {};
|
||||||
|
}
|
||||||
|
const unset = config();
|
||||||
|
assert.ok(unset.GEV_KEY_SETUP_TRUSTED_PEERS == null);
|
||||||
|
assert.ok(unset.GEV_ALLOWED_HOSTS == null);
|
||||||
|
const dotenv =
|
||||||
|
'GEV_KEY_SETUP_TRUSTED_PEERS=none\nGEV_ALLOWED_HOSTS=globe.example.lan\n';
|
||||||
|
assert.deepEqual(config(dotenv), {
|
||||||
|
GEV_KEY_SETUP_TRUSTED_PEERS: 'none',
|
||||||
|
GEV_ALLOWED_HOSTS: 'globe.example.lan',
|
||||||
|
});
|
||||||
|
assert.equal(
|
||||||
|
config(dotenv, { GEV_KEY_SETUP_TRUSTED_PEERS: '10.9.8.7' })
|
||||||
|
.GEV_KEY_SETUP_TRUSTED_PEERS,
|
||||||
|
'10.9.8.7',
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
config('', { GEV_KEY_SETUP_TRUSTED_PEERS: 'none' })
|
||||||
|
.GEV_KEY_SETUP_TRUSTED_PEERS,
|
||||||
|
'none',
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
config(dotenv, { GEV_KEY_SETUP_TRUSTED_PEERS: '' })
|
||||||
|
.GEV_KEY_SETUP_TRUSTED_PEERS,
|
||||||
|
'',
|
||||||
|
);
|
||||||
|
});
|
||||||
Reference in new issue
Block a user