Skip to content

Commit 2545956

Browse files
committed
fix: remove --disable-extensions-except to allow enterprise policy extensions
The --disable-extensions-except flag causes Chrome to set extensions_enabled_ to false, which prevents external providers (including the policy loader) from being created. This means Chrome never attempts to fetch force-installed extensions via ExtensionInstallForcelist enterprise policy. Changes: - Remove --disable-extensions-except from chromium.go flag generation - Remove --disable-extensions-except from wrapper.sh proxy extension setup - Update MergeExtensionPath to only use --load-extension - Update e2e test to upload kernel-like extension first (mirrors production) The fix allows enterprise policy extensions to be fetched and installed while still loading the kernel extension via --load-extension. See Chromium source: extension_service.cc - external providers are only created when extensions_enabled() returns true.
1 parent 2daa53f commit 2545956

File tree

4 files changed

+135
-7
lines changed

4 files changed

+135
-7
lines changed

images/chromium-headless/image/wrapper.sh

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,12 @@ fi
4444
export HOSTNAME="${HOSTNAME:-kernel-vm}"
4545

4646
# if CHROMIUM_FLAGS is not set, default to the flags used in playwright_stealth
47+
# NOTE: --disable-background-networking was intentionally removed because it prevents
48+
# Chrome from fetching extensions via ExtensionInstallForcelist enterprise policy.
49+
# Enterprise extensions require Chrome to make HTTP requests to fetch update.xml and .crx files.
4750
if [ -z "${CHROMIUM_FLAGS:-}" ]; then
48-
# NOTE: --disable-background-networking was intentionally removed because it prevents
49-
# Chrome from fetching extensions via ExtensionInstallForcelist enterprise policy.
50-
# Enterprise extensions require Chrome to make HTTP requests to fetch update.xml and .crx files.
51-
CHROMIUM_FLAGS="--accept-lang=en-US,en \
51+
CHROMIUM_FLAGS=" \
52+
--accept-lang=en-US,en \
5253
--allow-pre-commit-input \
5354
--blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 \
5455
--crash-dumps-dir=/tmp/chromium-dumps \
@@ -78,8 +79,10 @@ if [ -z "${CHROMIUM_FLAGS:-}" ]; then
7879
--enable-use-zoom-for-dsf=false \
7980
--export-tagged-pdf \
8081
--force-color-profile=srgb \
82+
--headless=new \
8183
--hide-crash-restore-bubble \
8284
--hide-scrollbars \
85+
--ignore-certificate-errors \
8386
--metrics-recording-only \
8487
--mute-audio \
8588
--no-default-browser-check \
@@ -88,10 +91,12 @@ if [ -z "${CHROMIUM_FLAGS:-}" ]; then
8891
--no-service-autorun \
8992
--ozone-platform=headless \
9093
--password-store=basic \
94+
--start-maximized \
9195
--unsafely-disable-devtools-self-xss-warnings \
9296
--use-angle=swiftshader \
9397
--use-gl=angle \
94-
--use-mock-keychain"
98+
--use-mock-keychain \
99+
--window-size=1512,982"
95100
fi
96101
export CHROMIUM_FLAGS
97102

@@ -117,7 +122,7 @@ if [[ "${RUN_AS_ROOT:-}" != "true" ]]; then
117122
done
118123

119124
# Ensure correct ownership (ignore errors if already correct)
120-
chown -R kernel:kernel /home/kernel /home/kernel/user-data /home/kernel/.config /home/kernel/.pki /home/kernel/.cache 2>/dev/null || true
125+
chown -R kernel:kernel /home/kernel /home/kernel/user-data /home/kernel/.config /home/kernel/.pki /home/kernel/.cache /home/kernel/extensions 2>/dev/null || true
121126
# Make policy directory writable for runtime updates
122127
chown -R kernel:kernel /etc/chromium/policies 2>/dev/null || true
123128
else
@@ -137,6 +142,39 @@ else
137142
done
138143
fi
139144

145+
# ------------------------------------------------------------------
146+
# Proxy extension setup ------------------------------------------------
147+
# ------------------------------------------------------------------
148+
# Determine whether we have proxy credentials
149+
have_proxy_creds=false
150+
if [[ -n "${PROXY_HOST:-}" ]] && [[ -n "${PROXY_PORT:-}" ]] \
151+
&& [[ -n "${PROXY_USERNAME:-}" ]] && [[ -n "${PROXY_PASSWORD:-}" ]]; then
152+
have_proxy_creds=true
153+
fi
154+
155+
# If proxy creds present, template the chromeproxy background script
156+
if [[ "$have_proxy_creds" == true ]]; then
157+
echo "Detected proxy configuration - preparing chromeproxy extension"
158+
EXT_BASE="/home/kernel/extensions"
159+
PROXY_AUTH_EXTENSION_PATH="$EXT_BASE/chromeproxy"
160+
BACKGROUND_JS="$PROXY_AUTH_EXTENSION_PATH/background.js"
161+
BACKGROUND_JS_TEMPLATE="$PROXY_AUTH_EXTENSION_PATH/background.js.template"
162+
163+
cp "$BACKGROUND_JS_TEMPLATE" "$BACKGROUND_JS"
164+
sed -i "s/PROXY_HOST/${PROXY_HOST}/g" "$BACKGROUND_JS"
165+
sed -i "s/PROXY_PORT/${PROXY_PORT}/g" "$BACKGROUND_JS"
166+
sed -i "s/PROXY_USERNAME/${PROXY_USERNAME}/g" "$BACKGROUND_JS"
167+
sed -i "s/PROXY_PASSWORD/${PROXY_PASSWORD}/g" "$BACKGROUND_JS"
168+
ext_list="$EXT_BASE/capmonster,$EXT_BASE/chromeproxy"
169+
# Append the necessary Chrome flags unless already present
170+
# NOTE: We intentionally do NOT use --disable-extensions-except here because it causes
171+
# Chrome to disable external providers (including the policy loader), which prevents
172+
# enterprise policy extensions (ExtensionInstallForcelist) from being fetched and installed.
173+
if [[ "${CHROMIUM_FLAGS:-}" != *"--load-extension="* ]]; then
174+
CHROMIUM_FLAGS="${CHROMIUM_FLAGS:-} --load-extension=${ext_list}"
175+
fi
176+
fi
177+
140178
# -----------------------------------------------------------------------------
141179
# Dynamic log aggregation for /var/log/supervisord -----------------------------
142180
# -----------------------------------------------------------------------------
@@ -207,6 +245,8 @@ for i in {1..30}; do
207245
sleep 0.2
208246
done
209247

248+
init-envoy.sh
249+
210250
echo "[wrapper] Starting system D-Bus daemon via supervisord"
211251
supervisorctl -c /etc/supervisor/supervisord.conf start dbus
212252
for i in {1..50}; do

server/cmd/api/api/chromium.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,10 +260,14 @@ func (s *ApiService) UploadExtensionsAndRestart(ctx context.Context, request oap
260260

261261
// Build flags overlay file in /chromium/flags, merging with existing flags
262262
// Only add --load-extension flags for extensions that don't use policy installation
263+
// NOTE: We intentionally do NOT use --disable-extensions-except here because it causes
264+
// Chrome to disable external providers (including the policy loader), which prevents
265+
// enterprise policy extensions (ExtensionInstallForcelist) from being fetched and installed.
266+
// See Chromium source: extension_service.cc - external providers are only created when
267+
// extensions_enabled() returns true, which is false when --disable-extensions-except is used.
263268
var newTokens []string
264269
if len(pathsNeedingFlags) > 0 {
265270
newTokens = []string{
266-
fmt.Sprintf("--disable-extensions-except=%s", strings.Join(pathsNeedingFlags, ",")),
267271
fmt.Sprintf("--load-extension=%s", strings.Join(pathsNeedingFlags, ",")),
268272
}
269273
}

server/e2e/e2e_enterprise_extension_test.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,17 @@ func runEnterpriseExtensionTest(t *testing.T, image string) {
8282
_, err = waitDevtoolsWS(ctx)
8383
require.NoError(t, err, "devtools not ready")
8484

85+
// First upload a simple extension to simulate the kernel extension in production.
86+
// This causes Chrome to be launched with --load-extension, which mirrors production
87+
// where the kernel extension is always loaded before any enterprise extensions.
88+
logger.Info("[test]", "action", "uploading kernel-like extension first (to simulate prod)")
89+
uploadKernelLikeExtension(t, ctx, logger)
90+
91+
// Wait for Chrome to restart with the new flags
92+
time.Sleep(3 * time.Second)
93+
_, err = waitDevtoolsWS(ctx)
94+
require.NoError(t, err, "devtools not ready after kernel extension")
95+
8596
// Upload the enterprise test extension (with update.xml and .crx)
8697
logger.Info("[test]", "action", "uploading enterprise test extension (with update.xml and .crx)")
8798
uploadEnterpriseTestExtension(t, ctx, logger)
@@ -138,6 +149,47 @@ func runEnterpriseExtensionTest(t *testing.T, image string) {
138149
logger.Info("[test]", "result", "enterprise extension installation test completed")
139150
}
140151

152+
// uploadKernelLikeExtension uploads a simple extension to simulate the kernel extension.
153+
// In production, the kernel extension is always loaded before any enterprise extensions,
154+
// so this ensures the test mirrors that behavior.
155+
func uploadKernelLikeExtension(t *testing.T, ctx context.Context, logger *slog.Logger) {
156+
t.Helper()
157+
158+
client, err := apiClient()
159+
require.NoError(t, err, "failed to create API client")
160+
161+
// Get the path to the simple test extension (no webRequest, so no enterprise policy)
162+
extDir, err := filepath.Abs("test-extension")
163+
require.NoError(t, err, "failed to get absolute path to test-extension")
164+
165+
// Create zip of the extension
166+
extZip, err := zipDirToBytes(extDir)
167+
require.NoError(t, err, "failed to zip test extension")
168+
169+
// Upload extension
170+
var body bytes.Buffer
171+
w := multipart.NewWriter(&body)
172+
fw, err := w.CreateFormFile("extensions.zip_file", "kernel-like-ext.zip")
173+
require.NoError(t, err)
174+
_, err = io.Copy(fw, bytes.NewReader(extZip))
175+
require.NoError(t, err)
176+
err = w.WriteField("extensions.name", "kernel")
177+
require.NoError(t, err)
178+
err = w.Close()
179+
require.NoError(t, err)
180+
181+
start := time.Now()
182+
rsp, err := client.UploadExtensionsAndRestartWithBodyWithResponse(ctx, w.FormDataContentType(), &body)
183+
elapsed := time.Since(start)
184+
require.NoError(t, err, "uploadExtensionsAndRestart request error")
185+
186+
require.Equal(t, http.StatusCreated, rsp.StatusCode(),
187+
"expected 201 Created but got %d. Body: %s",
188+
rsp.StatusCode(), string(rsp.Body))
189+
190+
logger.Info("[kernel-ext]", "action", "uploaded kernel-like extension", "elapsed", elapsed.String())
191+
}
192+
141193
// uploadEnterpriseTestExtension uploads the test extension with update.xml and .crx files.
142194
// This should trigger enterprise policy handling via ExtensionInstallForcelist.
143195
func uploadEnterpriseTestExtension(t *testing.T, ctx context.Context, logger *slog.Logger) {

server/lib/chromiumflags/chromiumflags.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,38 @@ func MergeFlagsWithRuntimeTokens(baseFlags string, runtimeTokens []string) []str
184184
return MergeFlags(base, runtimeTokens)
185185
}
186186

187+
// MergeExtensionPath appends an extension path to existing --load-extension flags
188+
// within an args slice. If the flag exists, the path is appended to its comma-separated
189+
// list. If it doesn't exist, a new flag is added. This preserves other extensions that
190+
// may already be configured.
191+
//
192+
// NOTE: We intentionally do NOT use --disable-extensions-except here because it causes
193+
// Chrome to disable external providers (including the policy loader), which prevents
194+
// enterprise policy extensions (ExtensionInstallForcelist) from being fetched and installed.
195+
// See Chromium source: extension_service.cc - external providers are only created when
196+
// extensions_enabled() returns true, which is false when --disable-extensions-except is used.
197+
func MergeExtensionPath(args []string, extPath string) []string {
198+
foundLoad := false
199+
result := make([]string, 0, len(args)+1)
200+
201+
for _, arg := range args {
202+
switch {
203+
case strings.HasPrefix(arg, "--load-extension="):
204+
existing := strings.TrimPrefix(arg, "--load-extension=")
205+
result = append(result, "--load-extension="+existing+","+extPath)
206+
foundLoad = true
207+
default:
208+
result = append(result, arg)
209+
}
210+
}
211+
212+
if !foundLoad {
213+
result = append(result, "--load-extension="+extPath)
214+
}
215+
216+
return result
217+
}
218+
187219
// WriteFlagFile writes the provided tokens to the given path as JSON in the
188220
// form: { "flags": ["--foo", "--bar=1"] } with file mode 0644.
189221
// The function creates or truncates the file.

0 commit comments

Comments
 (0)