Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
3 changes: 3 additions & 0 deletions images/chromium-headful/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -308,4 +308,7 @@ COPY server/runtime/playwright-executor.ts /usr/local/lib/playwright-executor.ts

RUN useradd -m -s /bin/bash kernel

# Make policy directory writable for runtime updates
RUN chown -R kernel:kernel /etc/chromium/policies

ENTRYPOINT [ "/wrapper.sh" ]
3 changes: 3 additions & 0 deletions images/chromium-headless/image/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,9 @@ ENV WITHDOCKER=true
# Create a non-root user with a home directory
RUN useradd -m -s /bin/bash kernel

# Make policy directory writable for runtime updates
RUN chown -R kernel:kernel /etc/chromium/policies

# supervisor start scripts
COPY images/chromium-headless/image/start-xvfb.sh /images/chromium-headless/image/start-xvfb.sh
RUN chmod +x /images/chromium-headless/image/start-xvfb.sh
Expand Down
31 changes: 31 additions & 0 deletions server/cmd/api/api/chromium.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/onkernel/kernel-images/server/lib/chromiumflags"
"github.com/onkernel/kernel-images/server/lib/logger"
oapi "github.com/onkernel/kernel-images/server/lib/oapi"
"github.com/onkernel/kernel-images/server/lib/policy"
"github.com/onkernel/kernel-images/server/lib/ziputil"
)

Expand Down Expand Up @@ -160,6 +161,36 @@ func (s *ApiService) UploadExtensionsAndRestart(ctx context.Context, request oap
log.Info("installed extension", "name", p.name)
}

// Update enterprise policy for extensions that require it
for _, p := range items {
extensionPath := filepath.Join(extBase, p.name)
extensionID := policy.GenerateExtensionID(p.name)
manifestPath := filepath.Join(extensionPath, "manifest.json")

// Check if this extension requires enterprise policy
requiresEntPolicy, err := policy.RequiresEnterprisePolicy(manifestPath)
if err != nil {
log.Warn("failed to read manifest for policy check", "error", err, "extension", p.name)
// Continue with requiresEntPolicy = false
}

if requiresEntPolicy {
log.Info("extension requires enterprise policy", "name", p.name)
}

// Add to enterprise policy
if err := policy.AddExtension(extensionID, extensionPath, requiresEntPolicy); err != nil {
log.Error("failed to update enterprise policy", "error", err, "extension", p.name)
return oapi.UploadExtensionsAndRestart500JSONResponse{
InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{
Message: fmt.Sprintf("failed to update enterprise policy for %s: %v", p.name, err),
},
}, nil
}

log.Info("updated enterprise policy", "extension", p.name, "id", extensionID, "requiresEnterprisePolicy", requiresEntPolicy)
}

// Build flags overlay file in /chromium/flags, merging with existing flags
var paths []string
for _, p := range items {
Expand Down
149 changes: 149 additions & 0 deletions server/lib/policy/policy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package policy

import (
"encoding/json"
"fmt"
"os"
"sync"
)

const PolicyPath = "/etc/chromium/policies/managed/policy.json"

// Policy represents the Chrome enterprise policy structure
type Policy struct {
PasswordManagerEnabled bool `json:"PasswordManagerEnabled"`
AutofillCreditCardEnabled bool `json:"AutofillCreditCardEnabled"`
TranslateEnabled bool `json:"TranslateEnabled"`
ExtensionSettings map[string]ExtensionSetting `json:"ExtensionSettings"`
}

// ExtensionSetting represents settings for a specific extension
type ExtensionSetting struct {
InstallationMode string `json:"installation_mode,omitempty"`
Path string `json:"path,omitempty"`
AllowedTypes []string `json:"allowed_types,omitempty"`
InstallSources []string `json:"install_sources,omitempty"`
RuntimeBlockedHosts []string `json:"runtime_blocked_hosts,omitempty"`
RuntimeAllowedHosts []string `json:"runtime_allowed_hosts,omitempty"`
}

var (
policyMutex sync.Mutex
)

// ReadPolicy reads the current enterprise policy from disk
func ReadPolicy() (*Policy, error) {
policyMutex.Lock()
defer policyMutex.Unlock()

data, err := os.ReadFile(PolicyPath)
if err != nil {
if os.IsNotExist(err) {
// Return default policy if file doesn't exist
return &Policy{
PasswordManagerEnabled: false,
AutofillCreditCardEnabled: false,
TranslateEnabled: false,
ExtensionSettings: make(map[string]ExtensionSetting),
}, nil
}
return nil, fmt.Errorf("failed to read policy file: %w", err)
}

var policy Policy
if err := json.Unmarshal(data, &policy); err != nil {
return nil, fmt.Errorf("failed to parse policy file: %w", err)
}

return &policy, nil
}

// WritePolicy writes the policy to disk
func WritePolicy(policy *Policy) error {
policyMutex.Lock()
defer policyMutex.Unlock()

data, err := json.MarshalIndent(policy, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal policy: %w", err)
}

if err := os.WriteFile(PolicyPath, data, 0644); err != nil {
return fmt.Errorf("failed to write policy file: %w", err)
}

return nil
}

// AddExtension adds or updates an extension in the policy
// extensionID should be a stable identifier (can be derived from extension path)
func AddExtension(extensionID, extensionPath string, requiresEnterprisePolicy bool) error {
policy, err := ReadPolicy()
if err != nil {
return err
}

// Ensure the wildcard policy exists
if _, exists := policy.ExtensionSettings["*"]; !exists {
policy.ExtensionSettings["*"] = ExtensionSetting{
AllowedTypes: []string{"extension"},
InstallSources: []string{"*"},
}
}

// Add the specific extension
setting := ExtensionSetting{
Path: extensionPath,
}

// If the extension requires enterprise policy (like webRequestBlocking),
// set it as force_installed https://github.com/cloudflare/web-bot-auth/blob/main/examples/browser-extension/policy/policy.json.templ
if requiresEnterprisePolicy {
setting.InstallationMode = "force_installed"
// Allow all hosts for webRequest APIs
setting.RuntimeAllowedHosts = []string{"*://*/*"}
} else {
setting.InstallationMode = "normal_installed"
}

policy.ExtensionSettings[extensionID] = setting

return WritePolicy(policy)
}

// GenerateExtensionID returns a stable identifier for the extension policy.
// For ExtensionSettings with local paths, Chrome allows custom identifiers.
// We use the extension name because it's stable, readable, and matches the directory.
func GenerateExtensionID(extensionName string) string {
return extensionName
}

// RequiresEnterprisePolicy checks if an extension requires enterprise policy
// by examining its manifest.json for webRequestBlocking or webRequest permissions
func RequiresEnterprisePolicy(manifestPath string) (bool, error) {
manifestData, err := os.ReadFile(manifestPath)
if err != nil {
return false, err
}

var manifest map[string]interface{}
if err := json.Unmarshal(manifestData, &manifest); err != nil {
return false, err
}

// Check if permissions include webRequestBlocking or webRequest
perms, ok := manifest["permissions"].([]interface{})
if !ok {
return false, nil
}

for _, perm := range perms {
if permStr, ok := perm.(string); ok {
if permStr == "webRequestBlocking" || permStr == "webRequest" {
return true, nil
}
}
}

return false, nil
}
8 changes: 7 additions & 1 deletion shared/chromium-policies/managed/policy.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,11 @@
"PasswordManagerEnabled": false,
"AutofillCreditCardEnabled": false,
"TranslateEnabled": false,
"DefaultNotificationsSetting": 2
"DefaultNotificationsSetting": 2,
"ExtensionSettings": {
"*": {
"allowed_types": ["extension"],
"install_sources": ["*"]
}
}
}
Loading