@open-everywhere/create 0.7.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/package.json +3 -3
- package/src/steps/build-deploy.ts +2 -2
- package/template/.dockerignore +35 -0
- package/template/AGENTS.md +7 -0
- package/template/Dockerfile +60 -0
- package/template/README.md +13 -0
- package/template/agent/agent.ts +25 -8
- package/template/agent/instructions.md +6 -0
- package/template/agent/lib/register-config.ts +5 -1
- package/template/agent/memory/config/domains/org.ts +2 -2
- package/template/agent/model/models.ts +17 -0
- package/template/agent/model/stacks.ts +23 -6
- package/template/agent/settings/settings.ts +21 -2
- package/template/agent/spend/model-pricing.ts +3 -1
- package/template/claude/skills/quickstart-open-everywhere/SKILL.md +96 -9
- package/template/claude/skills/upgrade-open-everywhere/SKILL.md +16 -3
- package/template/deploy/egress-proxy/Dockerfile +30 -0
- package/template/deploy/egress-proxy/control/acl.ts +228 -0
- package/template/deploy/egress-proxy/control/handlers.ts +174 -0
- package/template/deploy/egress-proxy/control/helper.cjs +108 -0
- package/template/deploy/egress-proxy/control/server.ts +122 -0
- package/template/deploy/egress-proxy/entrypoint.sh +62 -0
- package/template/deploy/egress-proxy/gen-ca.sh +42 -0
- package/template/deploy/egress-proxy/install-ca.sh +39 -0
- package/template/deploy/egress-proxy/squid.conf +93 -0
- package/template/deploy/egress-proxy/tier-b.conf +42 -0
- package/template/docker-compose.yml +135 -0
- package/template/gitignore +4 -0
- package/template/package.json +4 -3
- package/template/roster.lock.json +9 -1
- package/template/scripts/goldens/roster-plan.txt +8 -0
- package/template/scripts/model-config-check.ts +81 -0
- package/template/scripts/ops-mcp.ts +1 -1
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Squid external_acl_type helper for the Open Everywhere egress proxy.
|
|
2
|
+
//
|
|
3
|
+
// Squid spawns this as a long-lived child, feeds it one whitespace-separated request line
|
|
4
|
+
// per lookup on stdin, and reads one response line on stdout. We use the concurrency
|
|
5
|
+
// (channel-id) protocol: each input line is `<id> <src> <host> <method> <path> <auth>` and each
|
|
6
|
+
// output line is `<id> OK`/`<id> ERR`, optionally with a `message="..."` and, for Tier B,
|
|
7
|
+
// a `authz="<header value>"` annotation Squid can stamp onto the forwarded request.
|
|
8
|
+
//
|
|
9
|
+
// It simply forwards the decision to the local control service (server.ts) over loopback.
|
|
10
|
+
// Plain Node, zero deps (no TS) so it needs no build step.
|
|
11
|
+
//
|
|
12
|
+
// Squid config wires it as (deploy/egress-proxy/squid.conf):
|
|
13
|
+
// external_acl_type oe_egress ttl=0 negative_ttl=0 grace=0 concurrency=64 children-max=8 \
|
|
14
|
+
// children-startup=1 \
|
|
15
|
+
// %SRC %DST %METHOD %PATH %>{Proxy-Authorization} \
|
|
16
|
+
// /usr/bin/node /opt/oe-egress/control/helper.cjs
|
|
17
|
+
// The last field is the request's Proxy-Authorization ("-" when absent). Only the APP's own
|
|
18
|
+
// model call carries one (OPE-2406); the control service decides it against the host-plane
|
|
19
|
+
// list, and every request without one against the session policy for its source IP.
|
|
20
|
+
// (Tier A, no MITM: a CONNECT carries no method and no path, so only %SRC and %DST decide it —
|
|
21
|
+
// handleLine below blanks the CONNECT method and the rule matches at host grain. A plain-HTTP
|
|
22
|
+
// request on port 80 still arrives with a real method and path, and a rule's method/path
|
|
23
|
+
// narrowing does apply to that one.)
|
|
24
|
+
|
|
25
|
+
const http = require("node:http");
|
|
26
|
+
|
|
27
|
+
const CONTROL_HOST = process.env.OE_EGRESS_CONTROL_HOST_INTERNAL || "127.0.0.1";
|
|
28
|
+
const CONTROL_PORT = Number(process.env.OE_EGRESS_CONTROL_PORT || 8080);
|
|
29
|
+
|
|
30
|
+
function query(src, host, method, path, auth) {
|
|
31
|
+
return new Promise((resolve) => {
|
|
32
|
+
const qs = new URLSearchParams({ src: src || "", host: host || "", method: method || "", path: path || "", auth: auth || "" });
|
|
33
|
+
const req = http.request(
|
|
34
|
+
{ host: CONTROL_HOST, port: CONTROL_PORT, path: `/acl?${qs.toString()}`, method: "GET", timeout: 2000 },
|
|
35
|
+
(res) => {
|
|
36
|
+
let buf = "";
|
|
37
|
+
res.on("data", (d) => (buf += d));
|
|
38
|
+
res.on("end", () => {
|
|
39
|
+
try {
|
|
40
|
+
resolve(JSON.parse(buf));
|
|
41
|
+
} catch {
|
|
42
|
+
resolve({ allow: false }); // fail-closed on a malformed control response
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
},
|
|
46
|
+
);
|
|
47
|
+
req.on("error", () => resolve({ allow: false })); // fail-closed if control is unreachable
|
|
48
|
+
req.on("timeout", () => {
|
|
49
|
+
req.destroy();
|
|
50
|
+
resolve({ allow: false });
|
|
51
|
+
});
|
|
52
|
+
req.end();
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function esc(v) {
|
|
57
|
+
return String(v).replace(/"/g, '\\"');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function handleLine(line) {
|
|
61
|
+
const trimmed = line.trim();
|
|
62
|
+
if (!trimmed) return;
|
|
63
|
+
// Concurrency protocol: first token is the channel id.
|
|
64
|
+
const parts = trimmed.split(/\s+/);
|
|
65
|
+
const id = parts.shift();
|
|
66
|
+
const [src, host, rawMethod, path, auth] = parts;
|
|
67
|
+
// A CONNECT is tunnel establishment, not the inner HTTP request — in Tier A (splice, no
|
|
68
|
+
// decryption) we only ever see CONNECT, so treat its method/path as UNKNOWN and match at
|
|
69
|
+
// host granularity. In Tier B (bump) Squid re-issues the decrypted GET/POST with a real
|
|
70
|
+
// method+path, which arrives here for narrowing + header injection.
|
|
71
|
+
const method = rawMethod && rawMethod.toUpperCase() === "CONNECT" ? "" : rawMethod;
|
|
72
|
+
const decision = await query(dec(src), dec(host), dec(method), dec(path), dec(auth));
|
|
73
|
+
if (decision && decision.allow) {
|
|
74
|
+
// Tier B: pass the Authorization header value back as an annotation Squid stamps.
|
|
75
|
+
let extra = "";
|
|
76
|
+
if (decision.headers && decision.headers.authorization) {
|
|
77
|
+
extra = ` authz="${esc(decision.headers.authorization)}"`;
|
|
78
|
+
} else if (decision.headers && decision.headers.Authorization) {
|
|
79
|
+
extra = ` authz="${esc(decision.headers.Authorization)}"`;
|
|
80
|
+
}
|
|
81
|
+
process.stdout.write(`${id} OK${extra}\n`);
|
|
82
|
+
} else {
|
|
83
|
+
process.stdout.write(`${id} ERR message="denied by open-everywhere egress policy"\n`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Squid URL-ENCODES the fields it passes (%PATH and friends can hold spaces); decode defensively.
|
|
88
|
+
function dec(v) {
|
|
89
|
+
if (v === undefined) return "";
|
|
90
|
+
try {
|
|
91
|
+
return decodeURIComponent(v);
|
|
92
|
+
} catch {
|
|
93
|
+
return v;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let backlog = "";
|
|
98
|
+
process.stdin.setEncoding("utf-8");
|
|
99
|
+
process.stdin.on("data", (chunk) => {
|
|
100
|
+
backlog += chunk;
|
|
101
|
+
let nl;
|
|
102
|
+
while ((nl = backlog.indexOf("\n")) >= 0) {
|
|
103
|
+
const line = backlog.slice(0, nl);
|
|
104
|
+
backlog = backlog.slice(nl + 1);
|
|
105
|
+
void handleLine(line);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
process.stdin.on("end", () => process.exit(0));
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// Control service for the Open Everywhere egress proxy.
|
|
2
|
+
//
|
|
3
|
+
// Two clients:
|
|
4
|
+
// 1. The app (agent/access/self-host-egress.ts) POSTs the compiled SandboxNetworkPolicy
|
|
5
|
+
// to POST /policy whenever it changes (session start + mid-turn git broker).
|
|
6
|
+
// 2. The Squid `external_acl_type` helper (helper.cjs) GETs /acl per request to decide
|
|
7
|
+
// allow/deny and fetch any Tier B credential-injection headers.
|
|
8
|
+
//
|
|
9
|
+
// Dependency-free: Node's built-in http only, runs under `node --experimental-strip-types`
|
|
10
|
+
// (Node 24) so no build step and no npm install in the proxy image. All decision logic
|
|
11
|
+
// lives in the pure, unit-tested acl.ts / handlers.ts.
|
|
12
|
+
//
|
|
13
|
+
// Endpoints:
|
|
14
|
+
// POST /policy { policy, sourceIp?, sessionId? } → store policy for a source IP
|
|
15
|
+
// POST /policy { plane: "host", hostPlane } → store the app's HOST-PLANE allow list
|
|
16
|
+
// DELETE /policy ?sourceIp=IP → drop a source's policy
|
|
17
|
+
// GET /acl ?src=&host=&method=&path=&auth= → { allow, headers } (helper only)
|
|
18
|
+
// GET /healthz → 200
|
|
19
|
+
//
|
|
20
|
+
// Auth: OE_EGRESS_PROXY_TOKEN is REQUIRED — the service refuses to start without it (there is
|
|
21
|
+
// no tokenless mode and no opt-out flag). POST/DELETE /policy require `Authorization: Bearer
|
|
22
|
+
// <token>`; the listener is on the same internal network as the sandbox, so a tokenless control
|
|
23
|
+
// plane would let a compromised sandbox POST itself an allow-all policy and walk out through the
|
|
24
|
+
// proxy. /acl carries the Tier B credential-injection headers, so it is LOOPBACK-ONLY: the
|
|
25
|
+
// handler refuses any /acl request whose peer is not 127.0.0.0/8 or ::1 (403). The in-container
|
|
26
|
+
// Squid helper (helper.cjs) calls it over 127.0.0.1 and is unaffected; a sandbox on the
|
|
27
|
+
// app-facing network therefore cannot read an injected credential back out.
|
|
28
|
+
|
|
29
|
+
import http from "node:http";
|
|
30
|
+
import { PolicyStore } from "./acl.ts";
|
|
31
|
+
import { handlePolicyPush, handleAcl, isLoopbackAddr, requireControlToken, bearerMatches } from "./handlers.ts";
|
|
32
|
+
|
|
33
|
+
const store = new PolicyStore();
|
|
34
|
+
|
|
35
|
+
/** Fail loud and early: no token ⇒ no control plane. */
|
|
36
|
+
function loadToken(): string {
|
|
37
|
+
try {
|
|
38
|
+
return requireControlToken(process.env.OE_EGRESS_PROXY_TOKEN);
|
|
39
|
+
} catch (err) {
|
|
40
|
+
console.error(`[oe-egress-control] FATAL: ${err instanceof Error ? err.message : String(err)}`);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const TOKEN = loadToken();
|
|
46
|
+
const PORT = Number(process.env.OE_EGRESS_CONTROL_PORT ?? 8080);
|
|
47
|
+
const HOST = process.env.OE_EGRESS_CONTROL_HOST ?? "0.0.0.0";
|
|
48
|
+
|
|
49
|
+
function authOk(req: http.IncomingMessage): boolean {
|
|
50
|
+
return bearerMatches(req.headers["authorization"], TOKEN);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function remoteAddr(req: http.IncomingMessage): string {
|
|
54
|
+
return req.socket.remoteAddress ?? "";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function send(res: http.ServerResponse, status: number, body: unknown): void {
|
|
58
|
+
const payload = typeof body === "string" ? body : JSON.stringify(body);
|
|
59
|
+
res.writeHead(status, { "content-type": typeof body === "string" ? "text/plain" : "application/json" });
|
|
60
|
+
res.end(payload);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function readJson(req: http.IncomingMessage): Promise<unknown> {
|
|
64
|
+
const chunks: Buffer[] = [];
|
|
65
|
+
for await (const c of req) chunks.push(c as Buffer);
|
|
66
|
+
const raw = Buffer.concat(chunks).toString("utf-8");
|
|
67
|
+
if (!raw) return undefined;
|
|
68
|
+
return JSON.parse(raw);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const server = http.createServer(async (req, res) => {
|
|
72
|
+
try {
|
|
73
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
74
|
+
const path = url.pathname;
|
|
75
|
+
|
|
76
|
+
if (req.method === "GET" && path === "/healthz") return send(res, 200, { ok: true, policies: store.size(), hostPlane: store.hostPlane() !== undefined });
|
|
77
|
+
|
|
78
|
+
if (path === "/policy" && req.method === "POST") {
|
|
79
|
+
let body: unknown;
|
|
80
|
+
try {
|
|
81
|
+
body = await readJson(req);
|
|
82
|
+
} catch {
|
|
83
|
+
return send(res, 400, { error: "invalid JSON" });
|
|
84
|
+
}
|
|
85
|
+
const result = handlePolicyPush(store, { body, remoteAddr: remoteAddr(req), authOk: authOk(req) });
|
|
86
|
+
if (result.keyed) console.log(`[oe-egress-control] policy set for ${result.keyed} (${store.size()} session source(s) active)`);
|
|
87
|
+
return send(res, result.status, result.body);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (path === "/policy" && req.method === "DELETE") {
|
|
91
|
+
if (!authOk(req)) return send(res, 401, { error: "unauthorized" });
|
|
92
|
+
const src = url.searchParams.get("sourceIp") ?? remoteAddr(req);
|
|
93
|
+
const removed = store.delete(src);
|
|
94
|
+
return send(res, 200, { ok: true, removed });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (path === "/acl" && req.method === "GET") {
|
|
98
|
+
// /acl returns Tier B credential-injection headers; it is loopback-only (the in-container
|
|
99
|
+
// Squid helper calls over 127.0.0.1). Refuse any non-loopback peer so a sandbox on the
|
|
100
|
+
// app-facing network cannot read injected credentials back. See handlers.ts:isLoopbackAddr.
|
|
101
|
+
if (!isLoopbackAddr(remoteAddr(req))) return send(res, 403, { error: "forbidden" });
|
|
102
|
+
const q = {
|
|
103
|
+
src: url.searchParams.get("src") ?? undefined,
|
|
104
|
+
host: url.searchParams.get("host") ?? undefined,
|
|
105
|
+
method: url.searchParams.get("method") ?? undefined,
|
|
106
|
+
path: url.searchParams.get("path") ?? undefined,
|
|
107
|
+
auth: url.searchParams.get("auth") ?? undefined,
|
|
108
|
+
};
|
|
109
|
+
const { decision } = handleAcl(store, q, TOKEN);
|
|
110
|
+
return send(res, 200, { allow: decision.allow, headers: decision.headers ?? null, matchedHost: decision.matchedHost ?? null });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return send(res, 404, { error: "not found" });
|
|
114
|
+
} catch (err) {
|
|
115
|
+
console.error(`[oe-egress-control] error: ${String(err)}`);
|
|
116
|
+
return send(res, 500, { error: "internal error" });
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
server.listen(PORT, HOST, () => {
|
|
121
|
+
console.log(`[oe-egress-control] listening on ${HOST}:${PORT} (auth required on /policy)`);
|
|
122
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Entrypoint for the oe-egress-proxy container: start the policy control service, wire the
|
|
3
|
+
# tier (A default / B if OE_EGRESS_MITM=1), then exec Squid in the foreground.
|
|
4
|
+
set -eu
|
|
5
|
+
|
|
6
|
+
CONTROL_PORT="${OE_EGRESS_CONTROL_PORT:-8080}"
|
|
7
|
+
MITM="${OE_EGRESS_MITM:-0}"
|
|
8
|
+
SQUID_CONF="/etc/squid/squid.conf"
|
|
9
|
+
|
|
10
|
+
echo "[oe-egress] starting (MITM=$MITM) ..."
|
|
11
|
+
|
|
12
|
+
# ── Control service (loopback + app-net): holds per-source policy, answers ACL callouts ──
|
|
13
|
+
# Node 24 runs the TypeScript directly (native type stripping) — no build step, no tsx.
|
|
14
|
+
node --experimental-strip-types /opt/oe-egress/control/server.ts &
|
|
15
|
+
CONTROL_PID=$!
|
|
16
|
+
|
|
17
|
+
# Wait for the control service to accept connections before Squid starts firing callouts.
|
|
18
|
+
i=0
|
|
19
|
+
while [ $i -lt 50 ]; do
|
|
20
|
+
if wget -q -O /dev/null "http://127.0.0.1:${CONTROL_PORT}/healthz" 2>/dev/null; then break; fi
|
|
21
|
+
i=$((i + 1)); sleep 0.2
|
|
22
|
+
done
|
|
23
|
+
|
|
24
|
+
# ── Fresh, writable copy of the config (the image ships it read-only) ──
|
|
25
|
+
cp /opt/oe-egress/squid.conf "$SQUID_CONF"
|
|
26
|
+
|
|
27
|
+
if [ "$MITM" = "1" ]; then
|
|
28
|
+
echo "[oe-egress] Tier B (MITM) enabled — generating CA + initialising cert DB."
|
|
29
|
+
sh /opt/oe-egress/gen-ca.sh
|
|
30
|
+
# Augment the forward-proxy listener with ssl-bump so CONNECTs can be decrypted.
|
|
31
|
+
sed -i \
|
|
32
|
+
"s#^http_port 3128\$#http_port 3128 ssl-bump generate-host-certificates=on dynamic_cert_mem_cache_size=4MB tls-cert=${OE_EGRESS_CA_DIR:-/opt/oe-egress/ca}/oe-egress-ca.crt tls-key=${OE_EGRESS_CA_DIR:-/opt/oe-egress/ca}/oe-egress-ca.key#" \
|
|
33
|
+
"$SQUID_CONF"
|
|
34
|
+
# Append the Tier B overlay directly (no conf.d include — see squid.conf).
|
|
35
|
+
cat /opt/oe-egress/tier-b.conf >> "$SQUID_CONF"
|
|
36
|
+
# Initialise the dynamic-cert DB used by security_file_certgen.
|
|
37
|
+
rm -rf /var/lib/squid/ssl_db
|
|
38
|
+
/usr/lib/squid/security_file_certgen -c -s /var/lib/squid/ssl_db -M 4MB || true
|
|
39
|
+
chown -R squid:squid /var/lib/squid 2>/dev/null || true
|
|
40
|
+
else
|
|
41
|
+
echo "[oe-egress] Tier A (SNI/CONNECT host allow-deny, no decryption)."
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
# Squid drops to the `squid` user. Give it ownership of its state dir, and chown the
|
|
45
|
+
# container's stdout/stderr pipes so `access_log stdio:/dev/stdout` works as non-root
|
|
46
|
+
# (opening /dev/stdout by path FATALs otherwise — see squid.conf).
|
|
47
|
+
chown -R squid:squid /var/lib/squid 2>/dev/null || true
|
|
48
|
+
chown squid:squid /dev/stdout /dev/stderr 2>/dev/null || true
|
|
49
|
+
|
|
50
|
+
# The denial log (OPE-314). Created unconditionally so `access_log stdio:/var/log/oe-egress/...`
|
|
51
|
+
# can always be opened — Squid FATALs on an access_log it cannot open, and the volume that shares
|
|
52
|
+
# this file with the app is optional. Unmounted, the file lives inside the container and nobody
|
|
53
|
+
# reads it; mounted, the app tails it (read-only) and turns denials into audit rows.
|
|
54
|
+
mkdir -p /var/log/oe-egress
|
|
55
|
+
chown -R squid:squid /var/log/oe-egress 2>/dev/null || true
|
|
56
|
+
|
|
57
|
+
# Squid needs its swap/cache dirs initialised once.
|
|
58
|
+
squid -N -f "$SQUID_CONF" -z 2>/dev/null || true
|
|
59
|
+
|
|
60
|
+
echo "[oe-egress] launching squid."
|
|
61
|
+
# -N = no daemon (stay in foreground so the container's lifecycle tracks squid).
|
|
62
|
+
exec squid -N -f "$SQUID_CONF" -d 1
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Generate the local MITM CA for Tier B (opt-in) of the Open Everywhere egress proxy.
|
|
3
|
+
#
|
|
4
|
+
# Produces, in the shared CA dir (default /opt/oe-egress/ca, a mounted volume in
|
|
5
|
+
# docker-compose.yml so the app container can read the cert too):
|
|
6
|
+
# oe-egress-ca.key — the CA private key (proxy-only; NEVER leaves the proxy container)
|
|
7
|
+
# oe-egress-ca.crt — the CA certificate (installed into the app/sandbox trust store by
|
|
8
|
+
# install-ca.sh so git/npm/curl trust the bumped TLS)
|
|
9
|
+
#
|
|
10
|
+
# Idempotent: if a CA already exists it is left untouched (re-running won't churn the cert
|
|
11
|
+
# and invalidate an already-installed trust anchor). Delete the files to rotate.
|
|
12
|
+
#
|
|
13
|
+
# SECURITY: the CA key can mint a cert for ANY host. It is a real secret — keep it inside
|
|
14
|
+
# the proxy container's volume, never commit it (deploy/egress-proxy/.gitignore blocks it),
|
|
15
|
+
# and treat a leak as "rotate immediately". Tier B is opt-in precisely because introducing a
|
|
16
|
+
# trusted CA into the guest is a real trust decision (see README client-compat caveats).
|
|
17
|
+
set -eu
|
|
18
|
+
|
|
19
|
+
CA_DIR="${OE_EGRESS_CA_DIR:-/opt/oe-egress/ca}"
|
|
20
|
+
CA_KEY="$CA_DIR/oe-egress-ca.key"
|
|
21
|
+
CA_CRT="$CA_DIR/oe-egress-ca.crt"
|
|
22
|
+
CA_CN="${OE_EGRESS_CA_CN:-Open Everywhere Egress Proxy CA}"
|
|
23
|
+
DAYS="${OE_EGRESS_CA_DAYS:-3650}"
|
|
24
|
+
|
|
25
|
+
mkdir -p "$CA_DIR"
|
|
26
|
+
|
|
27
|
+
if [ -f "$CA_KEY" ] && [ -f "$CA_CRT" ]; then
|
|
28
|
+
echo "[gen-ca] CA already present at $CA_DIR — leaving it untouched (delete to rotate)."
|
|
29
|
+
exit 0
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
echo "[gen-ca] generating a new local CA in $CA_DIR ..."
|
|
33
|
+
openssl genrsa -out "$CA_KEY" 4096
|
|
34
|
+
openssl req -x509 -new -nodes -key "$CA_KEY" -sha256 -days "$DAYS" \
|
|
35
|
+
-subj "/CN=$CA_CN" \
|
|
36
|
+
-addext "basicConstraints=critical,CA:TRUE" \
|
|
37
|
+
-addext "keyUsage=critical,keyCertSign,cRLSign" \
|
|
38
|
+
-out "$CA_CRT"
|
|
39
|
+
chmod 600 "$CA_KEY"
|
|
40
|
+
chmod 644 "$CA_CRT"
|
|
41
|
+
echo "[gen-ca] done. CA cert: $CA_CRT"
|
|
42
|
+
echo "[gen-ca] install it into the app/sandbox with install-ca.sh (see README)."
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Install the Tier B MITM CA into THIS container's trust store + configure the common
|
|
3
|
+
# clients (git/npm/curl) to trust and use the proxy. Run inside the APP container (and,
|
|
4
|
+
# for the docker() sandbox backend, bake it into the sandbox image) — NOT the proxy.
|
|
5
|
+
#
|
|
6
|
+
# Requires the CA cert produced by gen-ca.sh, shared via the mounted /opt/oe-egress/ca
|
|
7
|
+
# volume. No-op-safe to run more than once.
|
|
8
|
+
#
|
|
9
|
+
# ⚠ CLIENT-COMPAT CAVEATS (design flagged these — do not paper over them):
|
|
10
|
+
# - Tools that PIN certificates (some language package managers, apps with bundled CA
|
|
11
|
+
# stores that ignore the OS store) will still reject the bumped cert. Those hosts must
|
|
12
|
+
# be Tier-A-spliced (host allow, no decryption), not bumped.
|
|
13
|
+
# - Tools that IGNORE the proxy env (HTTPS_PROXY) bypass the proxy entirely. On the
|
|
14
|
+
# internal-only Docker network they then simply fail to connect (no WAN route) — the
|
|
15
|
+
# structural backstop turns a proxy-bypass into a hard failure, not a leak.
|
|
16
|
+
# - Node/npm read NODE_EXTRA_CA_CERTS, not always the OS store — set below.
|
|
17
|
+
set -eu
|
|
18
|
+
|
|
19
|
+
CA_CRT="${OE_EGRESS_CA_CRT:-/opt/oe-egress/ca/oe-egress-ca.crt}"
|
|
20
|
+
PROXY_URL="${OE_EGRESS_PROXY_HTTP:-http://oe-egress-proxy:3128}"
|
|
21
|
+
|
|
22
|
+
if [ ! -f "$CA_CRT" ]; then
|
|
23
|
+
echo "[install-ca] CA cert not found at $CA_CRT — run gen-ca.sh in the proxy first (Tier B)." >&2
|
|
24
|
+
exit 1
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
# 1. OS trust store (alpine: ca-certificates). curl + git (via libcurl) pick this up.
|
|
28
|
+
if command -v update-ca-certificates >/dev/null 2>&1; then
|
|
29
|
+
cp "$CA_CRT" /usr/local/share/ca-certificates/oe-egress-ca.crt 2>/dev/null \
|
|
30
|
+
|| cp "$CA_CRT" /etc/ssl/certs/oe-egress-ca.pem
|
|
31
|
+
update-ca-certificates >/dev/null 2>&1 || true
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
echo "[install-ca] CA installed. Export these in the app environment (compose sets them):"
|
|
35
|
+
echo " HTTP_PROXY=$PROXY_URL"
|
|
36
|
+
echo " HTTPS_PROXY=$PROXY_URL"
|
|
37
|
+
echo " NODE_EXTRA_CA_CERTS=$CA_CRT # Node/npm trust the MITM CA"
|
|
38
|
+
echo " GIT_SSL_CAINFO=$CA_CRT # git trusts the MITM CA"
|
|
39
|
+
echo "[install-ca] (git also honors http.proxy=$PROXY_URL; npm honors https-proxy=$PROXY_URL)"
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Open Everywhere egress proxy — Squid base config.
|
|
2
|
+
#
|
|
3
|
+
# TIER A (default, NO MITM): a forward proxy that allows/denies each CONNECT (and plain
|
|
4
|
+
# HTTP) request by destination host, decided live by the control service via an
|
|
5
|
+
# external_acl callout. TLS is never decrypted — the raw bytes are tunnelled once the host
|
|
6
|
+
# passes (peek-and-splice semantics fall out of a plain CONNECT tunnel: Squid sees the
|
|
7
|
+
# CONNECT authority in cleartext and never terminates TLS). This restores the default-deny
|
|
8
|
+
# exfiltration backstop on justbash/docker without KVM.
|
|
9
|
+
#
|
|
10
|
+
# TIER B (opt-in, MITM) lives in conf.d/tier-b.conf, dropped in by entrypoint.sh only when
|
|
11
|
+
# OE_EGRESS_MITM=1. It ssl_bumps with a local CA so the decrypted request's method/path can
|
|
12
|
+
# be narrowed and a credential header injected.
|
|
13
|
+
#
|
|
14
|
+
# NB the exact external_acl format codes below (%SRC %DST %METHOD %PATH) are the widely
|
|
15
|
+
# supported named tokens; if your Squid build differs, adjust in one place here. See
|
|
16
|
+
# deploy/egress-proxy/README.md.
|
|
17
|
+
|
|
18
|
+
# Forward-proxy listener the app points HTTPS_PROXY/HTTP_PROXY at.
|
|
19
|
+
http_port 3128
|
|
20
|
+
|
|
21
|
+
# ── The live policy ACL: one callout per request to the control service (helper.cjs) ──
|
|
22
|
+
# ttl=0 → never cache a decision (the policy changes mid-turn via the git broker).
|
|
23
|
+
# %>{Proxy-Authorization} names the PRINCIPAL (OPE-2406). The app's own model call carries
|
|
24
|
+
# `Bearer <OE_EGRESS_PROXY_TOKEN>` and is decided against the app's host-plane allow list. A
|
|
25
|
+
# request without it is a session request, decided against the session policy for its source
|
|
26
|
+
# IP. Squid consumes the header here and does not forward it to the origin. No auth_param is
|
|
27
|
+
# configured, so Squid never answers 407 and an uncredentialed request is not challenged.
|
|
28
|
+
external_acl_type oe_egress ttl=0 negative_ttl=0 grace=0 concurrency=64 children-max=8 children-startup=1 \
|
|
29
|
+
%SRC %DST %METHOD %PATH %>{Proxy-Authorization} \
|
|
30
|
+
/usr/bin/node /opt/oe-egress/control/helper.cjs
|
|
31
|
+
acl oe_allowed external oe_egress
|
|
32
|
+
|
|
33
|
+
# ── Standard safety ACLs ──
|
|
34
|
+
acl CONNECT method CONNECT
|
|
35
|
+
acl SSL_ports port 443
|
|
36
|
+
acl Safe_ports port 80
|
|
37
|
+
acl Safe_ports port 443
|
|
38
|
+
|
|
39
|
+
http_access deny !Safe_ports
|
|
40
|
+
http_access deny CONNECT !SSL_ports
|
|
41
|
+
|
|
42
|
+
# ── The one allow rule: the control-service policy. Everything else is denied. ──
|
|
43
|
+
http_access allow oe_allowed
|
|
44
|
+
http_access deny all
|
|
45
|
+
|
|
46
|
+
# Policy gateway, not a cache.
|
|
47
|
+
cache deny all
|
|
48
|
+
via off
|
|
49
|
+
forwarded_for delete
|
|
50
|
+
|
|
51
|
+
# Squid drops privileges to the `squid` user. It logs to the container's stdout/stderr
|
|
52
|
+
# (below); the entrypoint chowns those pipes to `squid` so the non-root worker can open them
|
|
53
|
+
# (Squid 7 FATALs on `cache_effective_user root`, so dropping privileges is mandatory).
|
|
54
|
+
|
|
55
|
+
# Logs to the container's stdout/stderr for `docker logs`.
|
|
56
|
+
logfile_rotate 0
|
|
57
|
+
access_log stdio:/dev/stdout squid
|
|
58
|
+
|
|
59
|
+
# ── THE DENIAL LOG (OPE-314) — the ONE file the app is allowed to read ────────────────────────
|
|
60
|
+
# The app tails this to turn policy denials into `egress.denied` audit rows, which is what lets
|
|
61
|
+
# the permission-homeostasis widening loop see this plane at all (before OPE-314 an individual
|
|
62
|
+
# blocked request left no record anywhere — see packages/core/agent/access/egress-denial-log.ts).
|
|
63
|
+
#
|
|
64
|
+
# 🔴 THE MINIMISATION HAPPENS HERE, AT THE SOURCE, not only in the reader. The `squid` format
|
|
65
|
+
# above carries the full request URL, and under Tier B (MITM) Squid has DECRYPTED the request, so
|
|
66
|
+
# that URL includes the path AND the query string — the single most likely place for a token, an
|
|
67
|
+
# object id or a person's email to be sitting. This format carries three fields and no more:
|
|
68
|
+
# %ts.%03tu when (unix seconds + ms)
|
|
69
|
+
# %Ss/%03>Hs what Squid decided (e.g. TCP_DENIED/403 — the reader keys on the `_DENIED` tag)
|
|
70
|
+
# %>rd the DOMAIN (request URL domain — no scheme, no port, no path, no query)
|
|
71
|
+
# No client address, no method, no bytes, no user. The reader re-derives the host defensively
|
|
72
|
+
# anyway (it must also cope with an operator pointing it at a conventional access.log), but this
|
|
73
|
+
# line is why the bytes on the shared volume never contain a path in the first place.
|
|
74
|
+
#
|
|
75
|
+
# The `http_status 403` ACL is what narrows the file to refusals; a Squid ACL cannot match on %Ss.
|
|
76
|
+
# That is imprecise in one direction only — under Tier B an ORIGIN's own 403 also lands here as
|
|
77
|
+
# `TCP_MISS/403` — and the reader discards those, because only a `_DENIED` tag is OUR refusal.
|
|
78
|
+
#
|
|
79
|
+
# The directory is created by entrypoint.sh whether or not a volume is mounted over it, so Squid
|
|
80
|
+
# can never FATAL on an unopenable access_log; unmounted, the file simply stays inside the
|
|
81
|
+
# container and nothing reads it.
|
|
82
|
+
logformat oe_deny %ts.%03tu %Ss/%03>Hs %>rd
|
|
83
|
+
acl oe_deny_status http_status 403
|
|
84
|
+
access_log stdio:/var/log/oe-egress/denied.log oe_deny oe_deny_status
|
|
85
|
+
# cache_log (debug) also to stdout — the non-root `squid` user reliably reopens the chowned
|
|
86
|
+
# /dev/stdout pipe; /dev/stderr is flakier under Docker, and interleaving both on stdout is
|
|
87
|
+
# the conventional container pattern (`docker logs` captures it either way).
|
|
88
|
+
cache_log stdio:/dev/stdout
|
|
89
|
+
pid_filename /tmp/squid.pid
|
|
90
|
+
|
|
91
|
+
# Tier B (MITM) directives, if enabled, are APPENDED to this file by entrypoint.sh when
|
|
92
|
+
# OE_EGRESS_MITM=1 (not via an `include` glob — Squid 6.x treats a conf.d/*.conf glob that
|
|
93
|
+
# matches zero files as a FATAL error, which is exactly the Tier A case).
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Open Everywhere egress proxy — TIER B (opt-in MITM) overlay.
|
|
2
|
+
#
|
|
3
|
+
# Dropped into /etc/squid/conf.d/ by entrypoint.sh ONLY when OE_EGRESS_MITM=1. It makes
|
|
4
|
+
# Squid terminate TLS with the locally-generated CA (deploy/egress-proxy/ca, installed into
|
|
5
|
+
# the sandbox/guest trust store by gen-ca.sh) so that:
|
|
6
|
+
# 1. the decrypted request's method + path can be narrowed by the compiled policy, and
|
|
7
|
+
# 2. a Tier B credential header (the git read-broker Authorization) can be injected at the
|
|
8
|
+
# firewall — full parity with Vercel's transform.headers, secret never in-sandbox.
|
|
9
|
+
#
|
|
10
|
+
# ⚠ CLIENT COMPAT (see README): MITM only works for clients that (a) honor the proxy env
|
|
11
|
+
# (HTTPS_PROXY) and (b) trust the local CA. git/npm/curl do once the CA is installed and
|
|
12
|
+
# their proxy env is set; some tools pin certificates or bypass the proxy — those break
|
|
13
|
+
# under Tier B and must be allow-listed for Tier A splice instead.
|
|
14
|
+
|
|
15
|
+
# Bump listener — the CA cert+key generated by gen-ca.sh.
|
|
16
|
+
sslcrtd_program /usr/lib/squid/security_file_certgen -s /var/lib/squid/ssl_db -M 4MB
|
|
17
|
+
tls_outgoing_options options=NO_SSLv3
|
|
18
|
+
|
|
19
|
+
# Peek at the ClientHello (to read SNI for the ACL), then bump (decrypt) allowed hosts.
|
|
20
|
+
acl step1 at_step SslBump1
|
|
21
|
+
# THE HOST PLANE IS SPLICED AT STEP 1, never peeked or bumped (OPE-2406). The app's own model
|
|
22
|
+
# call carries Proxy-Authorization on the CONNECT only. After a peek, Squid re-checks a fake
|
|
23
|
+
# CONNECT built from the SNI, and a bumped inner request is a new request: neither carries the
|
|
24
|
+
# header, so the control service would decide it against the SESSION policy and deny the model
|
|
25
|
+
# call. `oe_proxy_cred` matches only a `Bearer <value>` header, and `oe_allowed` then passes only
|
|
26
|
+
# the valid token to a host-plane host. A missing header, or a literal `-` (which the control
|
|
27
|
+
# service reads as "no credential", the session shape), never matches, so a session request is
|
|
28
|
+
# peeked and bumped as before and cannot dodge Tier B's method/path narrowing by this rule.
|
|
29
|
+
acl oe_proxy_cred req_header Proxy-Authorization ^Bearer[[:space:]]+[^[:space:]]
|
|
30
|
+
ssl_bump splice step1 oe_proxy_cred oe_allowed
|
|
31
|
+
ssl_bump peek step1
|
|
32
|
+
ssl_bump bump oe_allowed
|
|
33
|
+
ssl_bump terminate all
|
|
34
|
+
|
|
35
|
+
# Inject the Tier B credential header the helper returned as the `authz` annotation.
|
|
36
|
+
# Only stamped when the ACL matched AND returned a header (the git read-broker path).
|
|
37
|
+
# The helper's `OK authz="…"` becomes a transaction note read here as %note{authz}
|
|
38
|
+
# (Squid 7 dropped %external_acl_message for request_header_add). The leading `'` is the
|
|
39
|
+
# LOG_QUOTE_NONE modifier — WITHOUT it Squid URL-encodes the note, turning the mandatory
|
|
40
|
+
# space in `Basic <base64>` / `Bearer <jwt>` into %20 and corrupting every real credential.
|
|
41
|
+
acl oe_has_authz note authz
|
|
42
|
+
request_header_add Authorization "%'note{authz}" oe_has_authz
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# This app on the Docker route: the app, the egress proxy, Postgres and the Postgres WebSocket bridge.
|
|
2
|
+
#
|
|
3
|
+
# THE TOPOLOGY IS THE EGRESS BACKSTOP:
|
|
4
|
+
# - `oe-net-internal` is `internal: true`, so it has NO route to the internet. The app is ONLY on it.
|
|
5
|
+
# - `oe-egress-proxy` is on `oe-net-internal` AND `oe-net-wan`. It is the one container with a route
|
|
6
|
+
# out, and its default is deny. So the app reaches the internet only through the proxy, even
|
|
7
|
+
# when a policy push fails.
|
|
8
|
+
#
|
|
9
|
+
# Before the first start:
|
|
10
|
+
# 1. `npm install` (the image builds from package-lock.json).
|
|
11
|
+
# 2. Put the deployment's values in `.env` (AI_GATEWAY_API_KEY, SLACK_*, OE_CREDENTIAL_KEY, ...).
|
|
12
|
+
# It must hold OE_EGRESS_PROXY_TOKEN (`openssl rand -hex 32`): without it compose refuses to
|
|
13
|
+
# start the stack. Set POSTGRES_PASSWORD there too; the default below is for a local trial only.
|
|
14
|
+
# Start: docker compose up --build
|
|
15
|
+
#
|
|
16
|
+
# The requirements this stack satisfies, and the ones it does not, are in the deployment contract:
|
|
17
|
+
# https://github.com/open-everywhere/open-everywhere/blob/main/docs/deploy/contract.md
|
|
18
|
+
# The egress proxy is documented in the self-host egress guide:
|
|
19
|
+
# https://github.com/open-everywhere/open-everywhere/blob/main/docs/deploy/platforms/self-host-egress.md
|
|
20
|
+
|
|
21
|
+
services:
|
|
22
|
+
oe-egress-proxy:
|
|
23
|
+
build:
|
|
24
|
+
context: ./deploy/egress-proxy
|
|
25
|
+
networks: [oe-net-internal, oe-net-wan]
|
|
26
|
+
environment:
|
|
27
|
+
OE_EGRESS_MITM: "${OE_EGRESS_MITM:-0}" # 1 = Tier B (MITM); the default is Tier A
|
|
28
|
+
# The shared secret for POST/DELETE /policy. `:?` makes compose refuse to start the stack when
|
|
29
|
+
# it is unset or empty. Without it, any peer on oe-net-internal could install an allow-all policy.
|
|
30
|
+
OE_EGRESS_PROXY_TOKEN: "${OE_EGRESS_PROXY_TOKEN:?set OE_EGRESS_PROXY_TOKEN in .env — the egress control plane refuses to start without it (generate: openssl rand -hex 32)}"
|
|
31
|
+
OE_EGRESS_CONTROL_PORT: "8080"
|
|
32
|
+
OE_EGRESS_CA_DIR: "/opt/oe-egress/ca"
|
|
33
|
+
volumes:
|
|
34
|
+
- oe-ca:/opt/oe-egress/ca # the CA, shared with the app (Tier B trust)
|
|
35
|
+
- oe-egress-log:/var/log/oe-egress # the denial log; the app reads it read-only
|
|
36
|
+
restart: unless-stopped
|
|
37
|
+
# No host port: the proxy is internal to the compose networks.
|
|
38
|
+
|
|
39
|
+
app:
|
|
40
|
+
build:
|
|
41
|
+
context: .
|
|
42
|
+
dockerfile: Dockerfile
|
|
43
|
+
depends_on:
|
|
44
|
+
oe-egress-proxy:
|
|
45
|
+
condition: service_started
|
|
46
|
+
postgres:
|
|
47
|
+
condition: service_healthy
|
|
48
|
+
neon-wsproxy:
|
|
49
|
+
condition: service_started
|
|
50
|
+
networks: [oe-net-internal] # internal ONLY: no direct route out
|
|
51
|
+
env_file:
|
|
52
|
+
- .env
|
|
53
|
+
environment:
|
|
54
|
+
# VERCEL stays UNSET: the self-host sandbox backend and the self-host egress push.
|
|
55
|
+
NODE_USE_ENV_PROXY: "1" # Node 24: built-in fetch uses *_PROXY
|
|
56
|
+
HTTP_PROXY: "http://oe-egress-proxy:3128"
|
|
57
|
+
HTTPS_PROXY: "http://oe-egress-proxy:3128"
|
|
58
|
+
# The policy push and the Postgres WebSocket go direct. `neon-wsproxy` MUST be here: Node 24
|
|
59
|
+
# also sends the driver's WebSocket through HTTP_PROXY, the proxy denies it, and the app exits
|
|
60
|
+
# at boot.
|
|
61
|
+
NO_PROXY: "oe-egress-proxy,neon-wsproxy,localhost,127.0.0.1"
|
|
62
|
+
# The app declares `sandbox.selfHostBackend: "justbash"` (agent/settings/settings.ts). Its
|
|
63
|
+
# session egress leaves through this app process, so the proxy holds every session request to
|
|
64
|
+
# the session's allow list. justbash shares this process's filesystem: development-grade
|
|
65
|
+
# isolation. The app refuses to boot here on `"docker"`: eve starts docker session containers
|
|
66
|
+
# on the daemon's default network, outside this proxy.
|
|
67
|
+
OE_EGRESS_PROXY_URL: "http://oe-egress-proxy:8080" # the control endpoint for the policy push
|
|
68
|
+
OE_EGRESS_PROXY_TOKEN: "${OE_EGRESS_PROXY_TOKEN:?set OE_EGRESS_PROXY_TOKEN in .env — the app's policy push is rejected without it (same value as the proxy)}"
|
|
69
|
+
# The proxy's denial log: a refused host becomes an `egress.denied` audit row. Unset it to turn
|
|
70
|
+
# the reader off.
|
|
71
|
+
OE_EGRESS_DENIAL_LOG: "${OE_EGRESS_DENIAL_LOG:-/var/log/oe-egress/denied.log}"
|
|
72
|
+
# Tier B: trust the MITM CA for git, npm and curl (no effect on Tier A).
|
|
73
|
+
NODE_EXTRA_CA_CERTS: "/opt/oe-egress/ca/oe-egress-ca.crt"
|
|
74
|
+
GIT_SSL_CAINFO: "/opt/oe-egress/ca/oe-egress-ca.crt"
|
|
75
|
+
# The operational store: the `postgres` sidecar below. This line wins over `.env`. For a
|
|
76
|
+
# managed Postgres, put its DATABASE_URL in .env, and delete this line, OE_NEON_WS_PROXY and
|
|
77
|
+
# the `postgres` and `neon-wsproxy` services.
|
|
78
|
+
DATABASE_URL: "postgres://openeverywhere:${POSTGRES_PASSWORD:-change-me-local-only}@postgres:5432/openeverywhere"
|
|
79
|
+
# The Postgres driver speaks WebSocket, not raw TCP, so it reaches the sidecar through
|
|
80
|
+
# `neon-wsproxy`. A hosted Neon database ignores this value.
|
|
81
|
+
OE_NEON_WS_PROXY: "${OE_NEON_WS_PROXY:-neon-wsproxy:80}"
|
|
82
|
+
PORT: "3000"
|
|
83
|
+
volumes:
|
|
84
|
+
- oe-memory:/app/.open-everywhere-memory # the git or sqlite memory backing
|
|
85
|
+
- oe-workflow:/app/.workflow-data # durable sessions
|
|
86
|
+
- oe-ca:/opt/oe-egress/ca:ro # the CA, for Tier B trust
|
|
87
|
+
- oe-egress-log:/var/log/oe-egress:ro # the proxy's denial log (READ-ONLY)
|
|
88
|
+
# Put a TLS-terminating reverse proxy in front of the app, and forward BOTH /eve/ and
|
|
89
|
+
# /.well-known/workflow/. To reach it directly for a local test, publish the port:
|
|
90
|
+
# ports: ["3000:3000"]
|
|
91
|
+
restart: unless-stopped
|
|
92
|
+
|
|
93
|
+
postgres:
|
|
94
|
+
# The same major version as hosted Neon.
|
|
95
|
+
image: postgres:17-alpine
|
|
96
|
+
networks: [oe-net-internal]
|
|
97
|
+
environment:
|
|
98
|
+
POSTGRES_USER: openeverywhere
|
|
99
|
+
POSTGRES_PASSWORD: "${POSTGRES_PASSWORD:-change-me-local-only}"
|
|
100
|
+
POSTGRES_DB: openeverywhere
|
|
101
|
+
volumes:
|
|
102
|
+
- oe-pg:/var/lib/postgresql/data
|
|
103
|
+
healthcheck:
|
|
104
|
+
test: ["CMD-SHELL", "pg_isready -U openeverywhere -d openeverywhere"]
|
|
105
|
+
interval: 5s
|
|
106
|
+
timeout: 3s
|
|
107
|
+
retries: 20
|
|
108
|
+
restart: unless-stopped
|
|
109
|
+
|
|
110
|
+
# Bridges the driver's WebSocket transport to the plain-TCP `postgres` sidecar.
|
|
111
|
+
neon-wsproxy:
|
|
112
|
+
image: ghcr.io/neondatabase/wsproxy:latest
|
|
113
|
+
networks: [oe-net-internal]
|
|
114
|
+
environment:
|
|
115
|
+
# EMPTY: the driver already sends `address=host:port`, and wsproxy appends this value to it.
|
|
116
|
+
APPEND_PORT: ""
|
|
117
|
+
ALLOW_ADDR_REGEX: "postgres:5432" # forward to the sidecar only
|
|
118
|
+
LOG_TRAFFIC: "false"
|
|
119
|
+
depends_on:
|
|
120
|
+
postgres:
|
|
121
|
+
condition: service_healthy
|
|
122
|
+
restart: unless-stopped
|
|
123
|
+
|
|
124
|
+
networks:
|
|
125
|
+
oe-net-internal:
|
|
126
|
+
internal: true # the backstop: NO route to the internet
|
|
127
|
+
oe-net-wan:
|
|
128
|
+
driver: bridge
|
|
129
|
+
|
|
130
|
+
volumes:
|
|
131
|
+
oe-memory:
|
|
132
|
+
oe-workflow:
|
|
133
|
+
oe-ca:
|
|
134
|
+
oe-egress-log:
|
|
135
|
+
oe-pg:
|
package/template/gitignore
CHANGED
|
@@ -42,5 +42,9 @@ node_modules/
|
|
|
42
42
|
*.pem
|
|
43
43
|
*.key
|
|
44
44
|
|
|
45
|
+
# The egress proxy's MITM CA (Tier B). It lives in a Docker volume; a copy on disk is a real secret.
|
|
46
|
+
deploy/egress-proxy/ca/
|
|
47
|
+
deploy/egress-proxy/*.crt
|
|
48
|
+
|
|
45
49
|
# A built Teams app package (`surface_app_package`): rebuilt from config, never committed.
|
|
46
50
|
teams-app/*.zip
|