Skip to content

Commit 60ae285

Browse files
committed
images: bake envoy proxy CA cert at build time
Moves openssl gen + system trust store install + NSS DB seed (for both root and kernel users) from runtime (init-envoy.sh) to image build time. The certs are static — CN=localhost, SANs localhost/127.0.0.1 — and trusted only by this image's CA store and chromium NSS DB, so sharing them across containers built from the same image does not widen the threat model. Full security rationale in bake-certs.sh. Net effect: Phase A no longer waits on a concurrent openssl/certutil shell-out, which was contending with xorg/dbus/chromedriver bring-up and adding ~1.7s to chromedriver readiness on test-mode boots. Removes the envoyCertsDone goroutine and the `init-envoy.sh certs` invocation entirely; init-envoy.sh now only renders the bootstrap template and starts envoy.
1 parent ac8c716 commit 60ae285

5 files changed

Lines changed: 126 additions & 136 deletions

File tree

images/chromium-headful/Dockerfile

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,4 +387,10 @@ RUN esbuild /tmp/playwright-daemon.ts \
387387

388388
RUN useradd -m -s /bin/bash kernel
389389

390+
# Bake the envoy forward-proxy CA cert into the image (system trust store +
391+
# NSS DB for both root and kernel users). See bake-certs.sh for the security
392+
# rationale on sharing the cert across containers built from this image.
393+
COPY shared/envoy/bake-certs.sh /usr/local/bin/bake-certs.sh
394+
RUN chmod +x /usr/local/bin/bake-certs.sh && /usr/local/bin/bake-certs.sh && rm /usr/local/bin/bake-certs.sh
395+
390396
ENTRYPOINT [ "/wrapper" ]

images/chromium-headless/image/Dockerfile

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,12 @@ COPY shared/envoy/bootstrap.yaml /etc/envoy/templates/bootstrap.yaml
240240
COPY shared/envoy/init-envoy.sh /usr/local/bin/init-envoy.sh
241241
RUN chmod +x /usr/local/bin/init-envoy.sh
242242

243+
# Bake the envoy forward-proxy CA cert into the image (system trust store +
244+
# NSS DB for both root and kernel users). See bake-certs.sh for the security
245+
# rationale on sharing the cert across containers built from this image.
246+
COPY shared/envoy/bake-certs.sh /usr/local/bin/bake-certs.sh
247+
RUN chmod +x /usr/local/bin/bake-certs.sh && /usr/local/bin/bake-certs.sh && rm /usr/local/bin/bake-certs.sh
248+
243249
# Copy the kernel-images API binary built in the builder stage
244250
COPY --from=server-builder /out/kernel-images-api /usr/local/bin/kernel-images-api
245251
COPY --from=server-builder /out/chromium-launcher /usr/local/bin/chromium-launcher

server/cmd/wrapper/main.go

Lines changed: 8 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -128,26 +128,12 @@ func main() {
128128
}
129129
waitForSocket(supervisorSock, 10*time.Second)
130130

131-
// Envoy cert work (openssl, update-ca-certificates, certutil) is the
132-
// only piece of Envoy bring-up that's identity-free, and it has to land
133-
// before chromium starts because chromium reads the system CA trust
134-
// store at process start. Run it concurrently with Phase A so the
135-
// shell-out cost overlaps xorg/dbus/chromedriver bring-up. Template
136-
// render and `supervisorctl start envoy` happen later in Phase C —
137-
// those depend on INST_NAME/METRO_NAME/XDS_SERVER/KERNEL_INSTANCE_JWT.
138-
envoyCertsDone := make(chan struct{})
139-
if isExecutable("/usr/local/bin/init-envoy.sh") {
140-
go func() {
141-
defer close(envoyCertsDone)
142-
runStream("envoy-init", "/usr/local/bin/init-envoy.sh", "certs")
143-
}()
144-
} else {
145-
close(envoyCertsDone)
146-
}
147-
148131
// Phase A: identity-free services with no X/dbus dependency. chromedriver
149132
// listens on 9225 immediately and only attaches to chromium on session
150-
// creation, so it can come up alongside the display stack.
133+
// creation, so it can come up alongside the display stack. The envoy
134+
// forward-proxy CA cert is baked into the image at build time (see
135+
// shared/envoy/bake-certs.sh), so chromium trusts it on first start with
136+
// no runtime cert work to wait on.
151137
xServer := "xorg"
152138
if prof == profileHeadless {
153139
xServer = "xvfb"
@@ -161,13 +147,10 @@ func main() {
161147
// parallel with chromium.
162148
_ = os.WriteFile(filepath.Join(supervisordLogD, "chromium"), nil, 0o644)
163149

164-
// Gate chromium on the envoy cert being installed in the trust store.
165-
<-envoyCertsDone
166-
167150
// Phase B: identity-free X/dbus consumers. Chromium itself doesn't read
168-
// any per-instance identity envs — it just needs the envoy cert (Phase A)
169-
// in trust. mutter is the compositor on headful; neko is the WebRTC
170-
// streamer when ENABLE_WEBRTC=true.
151+
// any per-instance identity envs — it just needs the envoy cert (baked
152+
// into the image) in trust. mutter is the compositor on headful; neko is
153+
// the WebRTC streamer when ENABLE_WEBRTC=true.
171154
webrtc := prof == profileHeadful && os.Getenv("ENABLE_WEBRTC") == "true"
172155
var phaseB []string
173156
if prof == profileHeadful {
@@ -203,7 +186,7 @@ func main() {
203186
// so the same code path works for boot (start a stopped service) and
204187
// post-fork (stop+start to force a re-read of refreshed envs).
205188
if isExecutable("/usr/local/bin/init-envoy.sh") {
206-
runStream("envoy-init", "/usr/local/bin/init-envoy.sh", "config")
189+
runStream("envoy-init", "/usr/local/bin/init-envoy.sh")
207190
}
208191
restartAll("kernel-images-api")
209192

shared/envoy/bake-certs.sh

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
#!/bin/bash
2+
set -eux
3+
4+
# Generate the self-signed cert envoy presents on the localhost forward proxy,
5+
# install it into the system CA trust store, and seed the NSS DBs for both the
6+
# root and `kernel` users so chromium trusts it regardless of which user the
7+
# wrapper runs as. Runs once at image build time so container startup pays
8+
# zero cost — no openssl invocation, no certutil shell-outs.
9+
#
10+
# Safety of a shared (per-image-tag) cert across customer instances:
11+
# - The cert's only Subject Alternative Names are `DNS:localhost` and
12+
# `IP:127.0.0.1`. A TLS client only accepts it for connections to
13+
# localhost, so the cert (and its private key) are useless for MITMing
14+
# any traffic the cert holder doesn't already control.
15+
# - The cert is trusted only by this image's system CA store and chromium
16+
# NSS DB. It is not trusted by customer machines, the host, or anything
17+
# outside this container.
18+
# - The forward proxy listens on 127.0.0.1 inside a network-isolated
19+
# container. One customer's container has no path to another customer's
20+
# localhost. Even an attacker holding the private key would need code
21+
# execution inside a sibling container to use it, at which point they
22+
# have everything anyway.
23+
# - The cert never leaves the container — no customer SDK, no browser
24+
# extension, no host service ever sees it.
25+
# Bottom line: this CA is an in-container trust anchor for a localhost-only
26+
# TLS listener. Sharing the key across containers built from the same image
27+
# does not widen the threat model.
28+
29+
mkdir -p /etc/envoy/certs
30+
openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \
31+
-keyout /etc/envoy/certs/proxy.key \
32+
-out /etc/envoy/certs/proxy.crt \
33+
-subj "/C=US/ST=CA/O=Kernel/CN=localhost" \
34+
-addext "subjectAltName = DNS:localhost,IP:127.0.0.1"
35+
36+
# System trust store — picked up by curl, openssl, Go's net/http, etc.
37+
cp /etc/envoy/certs/proxy.crt /usr/local/share/ca-certificates/kernel-envoy-proxy.crt
38+
cp /etc/envoy/certs/proxy.crt /kernel-envoy-proxy.crt
39+
40+
# Seed both NSS DBs so chromium trusts the cert under either user. The
41+
# wrapper's RUN_AS_ROOT branch chooses which DB chromium reads from at
42+
# runtime; seeding both at build time means we don't need to know yet.
43+
mkdir -p /root/.pki/nssdb /home/kernel/.pki/nssdb
44+
certutil -d /root/.pki/nssdb -N --empty-password 2>/dev/null || true
45+
certutil -d /home/kernel/.pki/nssdb -N --empty-password 2>/dev/null || true
46+
certutil -d /root/.pki/nssdb -A -t "C,," -n "Kernel Envoy Proxy" -i /etc/envoy/certs/proxy.crt
47+
certutil -d /home/kernel/.pki/nssdb -A -t "C,," -n "Kernel Envoy Proxy" -i /etc/envoy/certs/proxy.crt
48+
49+
# Install any pre-baked CA certs (BrightData certs are downloaded into
50+
# /etc/envoy/brightdata by install-proxy.sh in private images). Same
51+
# identity-free trust-store work as the self-signed cert above — moving it
52+
# here means runtime sees an already-populated trust store.
53+
if [ -d /etc/envoy/brightdata ]; then
54+
for cert in /etc/envoy/brightdata/*.crt; do
55+
[ -f "$cert" ] || continue
56+
cert_name=$(basename "$cert" .crt)
57+
cp "$cert" "/usr/local/share/ca-certificates/brightdata-${cert_name}.crt"
58+
certutil -d /root/.pki/nssdb -A -t "C,," -n "BrightData $cert_name" -i "$cert"
59+
certutil -d /home/kernel/.pki/nssdb -A -t "C,," -n "BrightData $cert_name" -i "$cert"
60+
done
61+
fi
62+
63+
chown -R kernel:kernel /home/kernel/.pki
64+
65+
update-ca-certificates

shared/envoy/init-envoy.sh

Lines changed: 41 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -2,114 +2,44 @@
22

33
set -o pipefail -o errexit -o nounset
44

5-
# Phase argument lets the Go wrapper split the script into an identity-free
6-
# stage (certs/CA trust/NSS DB — runs early so chromium boots with the cert
7-
# already trusted) and an identity-bound stage (template render with
8-
# INST_NAME/METRO_NAME/XDS_SERVER/KERNEL_INSTANCE_JWT, then envoy start).
9-
# certs — generate self-signed cert and install it in trust stores
10-
# config — render bootstrap template and start envoy via supervisord
11-
# all — both phases (default; preserves legacy single-call behavior)
12-
PHASE="${1:-all}"
13-
14-
case "$PHASE" in
15-
certs|config|all) ;;
16-
*)
17-
echo "[envoy-init] Unknown phase: $PHASE (expected certs|config|all)" >&2
18-
exit 2
19-
;;
20-
esac
21-
22-
run_certs() {
23-
if [[ ! -f /etc/envoy/templates/bootstrap.yaml ]]; then
24-
echo "[envoy-init] Template file /etc/envoy/templates/bootstrap.yaml not found. Skipping cert generation."
25-
return 0
26-
fi
27-
28-
echo "[envoy-init] Generating self-signed certificates for TLS forward proxy"
29-
mkdir -p /etc/envoy/certs
30-
31-
if [[ -f /etc/envoy/certs/proxy.crt && -f /etc/envoy/certs/proxy.key ]]; then
32-
echo "[envoy-init] Certificates already exist, skipping generation"
33-
return 0
34-
fi
35-
36-
echo "[envoy-init] Creating new self-signed certificate"
37-
openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \
38-
-keyout /etc/envoy/certs/proxy.key \
39-
-out /etc/envoy/certs/proxy.crt \
40-
-subj "/C=US/ST=CA/O=Kernel/CN=localhost" \
41-
-addext "subjectAltName = DNS:localhost,IP:127.0.0.1" \
42-
2>&1 | sed 's/^/[envoy-init] /'
43-
echo "[envoy-init] Certificate generated successfully"
44-
45-
echo "[envoy-init] Adding certificate to system trust store"
46-
cp /etc/envoy/certs/proxy.crt /usr/local/share/ca-certificates/kernel-envoy-proxy.crt
47-
cp /etc/envoy/certs/proxy.crt /kernel-envoy-proxy.crt
48-
update-ca-certificates 2>&1 | sed 's/^/[envoy-init] /'
49-
echo "[envoy-init] Certificate added to system trust store"
50-
51-
if [[ "${RUN_AS_ROOT:-}" == "true" ]]; then
52-
mkdir -p /root/.pki/nssdb
53-
certutil -d /root/.pki/nssdb -N --empty-password 2>/dev/null || true
54-
certutil -d /root/.pki/nssdb -A -t "C,," -n "Kernel Envoy Proxy" -i /etc/envoy/certs/proxy.crt
55-
echo "[envoy-init] Certificate added to nssdb as root"
56-
else
57-
mkdir -p /home/kernel/.pki/nssdb
58-
certutil -d /home/kernel/.pki/nssdb -N --empty-password 2>/dev/null || true
59-
certutil -d /home/kernel/.pki/nssdb -A -t "C,," -n "Kernel Envoy Proxy" -i /etc/envoy/certs/proxy.crt
60-
chown -R kernel:kernel /home/kernel/.pki
61-
echo "[envoy-init] Certificate added to nssdb as kernel"
62-
fi
63-
}
64-
65-
run_config() {
66-
# Identity envs gate the config phase: without them xDS can't bind, so
67-
# render+start is a no-op on images that don't run with a JWT.
68-
INSTANCE_JWT="${KERNEL_INSTANCE_JWT:-}"
69-
if [[ -z "${INST_NAME:-}" || -z "${METRO_NAME:-}" || -z "${XDS_SERVER:-}" || -z "${INSTANCE_JWT:-}" ]]; then
70-
echo "[envoy-init] Required environment variables not set. Skipping Envoy config/start."
71-
return 0
72-
fi
73-
74-
if [[ ! -f /etc/envoy/templates/bootstrap.yaml ]]; then
75-
echo "[envoy-init] Template file /etc/envoy/templates/bootstrap.yaml not found. Skipping Envoy config/start."
76-
return 0
77-
fi
78-
79-
echo "[envoy-init] Preparing Envoy bootstrap configuration"
80-
mkdir -p /etc/envoy
81-
82-
echo "[envoy-init] Rendering template with INST_NAME=${INST_NAME}, METRO_NAME=${METRO_NAME}, XDS_SERVER=${XDS_SERVER}, KERNEL_INSTANCE_JWT=***"
83-
inst_esc=$(printf '%s' "$INST_NAME" | sed -e 's/[\/&]/\\&/g')
84-
metro_esc=$(printf '%s' "$METRO_NAME" | sed -e 's/[\/&]/\\&/g')
85-
xds_esc=$(printf '%s' "$XDS_SERVER" | sed -e 's/[\/&]/\\&/g')
86-
jwt_esc=$(printf '%s' "$INSTANCE_JWT" | sed -e 's/[\/&]/\\&/g')
87-
sed -e "s|{INST_NAME}|$inst_esc|g" \
88-
-e "s|{METRO_NAME}|$metro_esc|g" \
89-
-e "s|{XDS_SERVER}|$xds_esc|g" \
90-
-e "s|{KERNEL_INSTANCE_JWT}|$jwt_esc|g" \
91-
/etc/envoy/templates/bootstrap.yaml > /etc/envoy/bootstrap.yaml
92-
93-
echo "[envoy-init] Starting Envoy via supervisord"
94-
# `restart` is start-or-stop+start: on first boot this just starts envoy,
95-
# on a re-render (e.g. post-fork env refresh) it forces a clean re-read
96-
# of the rendered bootstrap. Either way no callers see stale identity.
97-
supervisorctl -c /etc/supervisor/supervisord.conf restart envoy
98-
99-
# Readiness (port 3128 reachable) is probed by the Go wrapper's
100-
# waitAllReady alongside CDP/chromedriver, so this script returns as soon
101-
# as the start request has been issued.
102-
}
103-
104-
case "$PHASE" in
105-
certs)
106-
run_certs
107-
;;
108-
config)
109-
run_config
110-
;;
111-
all)
112-
run_certs
113-
run_config
114-
;;
115-
esac
5+
# Runtime config for envoy. Cert generation and CA trust install ran at image
6+
# build time (see shared/envoy/bake-certs.sh) so this script only does the
7+
# identity-bound work: render the bootstrap template with the per-instance
8+
# envs and start envoy via supervisord.
9+
10+
# Identity envs gate this script: without them xDS can't bind, so this is a
11+
# no-op on images that don't run with a JWT.
12+
INSTANCE_JWT="${KERNEL_INSTANCE_JWT:-}"
13+
if [[ -z "${INST_NAME:-}" || -z "${METRO_NAME:-}" || -z "${XDS_SERVER:-}" || -z "${INSTANCE_JWT:-}" ]]; then
14+
echo "[envoy-init] Required environment variables not set. Skipping Envoy config/start."
15+
exit 0
16+
fi
17+
18+
if [[ ! -f /etc/envoy/templates/bootstrap.yaml ]]; then
19+
echo "[envoy-init] Template file /etc/envoy/templates/bootstrap.yaml not found. Skipping Envoy config/start."
20+
exit 0
21+
fi
22+
23+
echo "[envoy-init] Preparing Envoy bootstrap configuration"
24+
mkdir -p /etc/envoy
25+
26+
echo "[envoy-init] Rendering template with INST_NAME=${INST_NAME}, METRO_NAME=${METRO_NAME}, XDS_SERVER=${XDS_SERVER}, KERNEL_INSTANCE_JWT=***"
27+
inst_esc=$(printf '%s' "$INST_NAME" | sed -e 's/[\/&]/\\&/g')
28+
metro_esc=$(printf '%s' "$METRO_NAME" | sed -e 's/[\/&]/\\&/g')
29+
xds_esc=$(printf '%s' "$XDS_SERVER" | sed -e 's/[\/&]/\\&/g')
30+
jwt_esc=$(printf '%s' "$INSTANCE_JWT" | sed -e 's/[\/&]/\\&/g')
31+
sed -e "s|{INST_NAME}|$inst_esc|g" \
32+
-e "s|{METRO_NAME}|$metro_esc|g" \
33+
-e "s|{XDS_SERVER}|$xds_esc|g" \
34+
-e "s|{KERNEL_INSTANCE_JWT}|$jwt_esc|g" \
35+
/etc/envoy/templates/bootstrap.yaml > /etc/envoy/bootstrap.yaml
36+
37+
echo "[envoy-init] Starting Envoy via supervisord"
38+
# `restart` is start-or-stop+start: on first boot this just starts envoy,
39+
# on a re-render (e.g. post-fork env refresh) it forces a clean re-read
40+
# of the rendered bootstrap. Either way no callers see stale identity.
41+
supervisorctl -c /etc/supervisor/supervisord.conf restart envoy
42+
43+
# Readiness (port 3128 reachable) is probed by the Go wrapper's
44+
# waitAllReady alongside CDP/chromedriver, so this script returns as soon
45+
# as the start request has been issued.

0 commit comments

Comments
 (0)