HH-743: harden browser image and lifecycle (#5)
This commit is contained in:
+3
-3
@@ -1,18 +1,18 @@
|
||||
FROM node:22-alpine AS web
|
||||
FROM node:22-alpine@sha256:c610fcdfb1d5b4740dd70c284ed3cb16bb857e0f7166196e36a5501df7a3aa32 AS web
|
||||
WORKDIR /src/web
|
||||
COPY web/package*.json ./
|
||||
RUN npm ci
|
||||
COPY web/ ./
|
||||
RUN npm run build
|
||||
|
||||
FROM golang:1.26-alpine AS go
|
||||
FROM golang:1.26-alpine@sha256:28d89ee9cc0ff9fec75c82ca201e6bf7fdf9a679d4b7b24dfa04f2bb766bb468 AS go
|
||||
WORKDIR /src
|
||||
COPY go.mod ./
|
||||
COPY cmd/ ./cmd/
|
||||
RUN CGO_ENABLED=0 go build -trimpath -ldflags='-s -w' -o /out/control-plane ./cmd/control-plane \
|
||||
&& CGO_ENABLED=0 go build -trimpath -ldflags='-s -w' -o /out/docker-gateway ./cmd/docker-gateway
|
||||
|
||||
FROM alpine:3.22
|
||||
FROM alpine:3.22@sha256:14358309a308569c32bdc37e2e0e9694be33a9d99e68afb0f5ff33cc1f695dce
|
||||
RUN addgroup -g 65532 app && adduser -D -u 65532 -G app app
|
||||
WORKDIR /app
|
||||
COPY --from=go /out/control-plane /out/docker-gateway /app/
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
需要 Docker Compose。控制面仅绑定本机;浏览器网络在阶段 A 默认禁止外网访问:
|
||||
|
||||
```bash
|
||||
docker pull git.ipao.vip/rogee/fingerprint-chromium:148.0.7778.215
|
||||
docker pull git.ipao.vip/rogee/fingerprint-chromium@sha256:b9f23b8e3ac640174db0dfa49e9095fe7eb06f5db55a4e7550d979b35ff3a1b7
|
||||
DOCKER_GID=$(stat -c %g /var/run/docker.sock) docker compose up --build
|
||||
```
|
||||
|
||||
@@ -28,6 +28,7 @@ go test ./...
|
||||
npm --prefix web ci
|
||||
npm --prefix web run build
|
||||
docker compose config --quiet
|
||||
CREATORHUB_DOCKER_INTEGRATION=1 go test -run TestDockerIntegrationBlocksCrossContainerTraffic ./cmd/docker-gateway
|
||||
```
|
||||
|
||||
## 合规边界
|
||||
|
||||
+12
-10
@@ -16,7 +16,17 @@ func main() {
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
server := &http.Server{
|
||||
Addr: env("LISTEN_ADDR", ":8080"),
|
||||
Handler: newHandler(gateway, env("WEB_DIR", "web/dist")),
|
||||
ReadHeaderTimeout: 5 * time.Second,
|
||||
IdleTimeout: 60 * time.Second,
|
||||
}
|
||||
log.Printf("CreatorHub control plane listening on %s", server.Addr)
|
||||
log.Fatal(server.ListenAndServe())
|
||||
}
|
||||
|
||||
func newHandler(gateway *url.URL, webDirectory string) http.Handler {
|
||||
proxy := httputil.NewSingleHostReverseProxy(gateway)
|
||||
originalDirector := proxy.Director
|
||||
proxy.Director = func(request *http.Request) {
|
||||
@@ -31,16 +41,8 @@ func main() {
|
||||
mux.HandleFunc("GET /healthz", func(response http.ResponseWriter, _ *http.Request) {
|
||||
response.WriteHeader(http.StatusNoContent)
|
||||
})
|
||||
mux.Handle("/", spaHandler(env("WEB_DIR", "web/dist")))
|
||||
|
||||
server := &http.Server{
|
||||
Addr: env("LISTEN_ADDR", ":8080"),
|
||||
Handler: mux,
|
||||
ReadHeaderTimeout: 5 * time.Second,
|
||||
IdleTimeout: 60 * time.Second,
|
||||
}
|
||||
log.Printf("CreatorHub control plane listening on %s", server.Addr)
|
||||
log.Fatal(server.ListenAndServe())
|
||||
mux.Handle("/", spaHandler(webDirectory))
|
||||
return mux
|
||||
}
|
||||
|
||||
func spaHandler(directory string) http.Handler {
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAPIProxyPreservesMethodPathAndStatus(t *testing.T) {
|
||||
requests := make(chan *http.Request, 1)
|
||||
gateway := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
|
||||
requests <- request
|
||||
response.WriteHeader(http.StatusServiceUnavailable)
|
||||
}))
|
||||
defer gateway.Close()
|
||||
|
||||
upstream, err := url.Parse(gateway.URL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
request := httptest.NewRequest(http.MethodPost, "/api/browsers/account-a/start", nil)
|
||||
response := httptest.NewRecorder()
|
||||
newHandler(upstream, t.TempDir()).ServeHTTP(response, request)
|
||||
|
||||
proxied := <-requests
|
||||
if proxied.Method != http.MethodPost || proxied.URL.Path != "/v1/browsers/account-a/start" {
|
||||
t.Fatalf("unexpected proxy request %s %s", proxied.Method, proxied.URL.Path)
|
||||
}
|
||||
if response.Code != http.StatusServiceUnavailable {
|
||||
t.Fatalf("expected upstream status 503, got %d", response.Code)
|
||||
}
|
||||
}
|
||||
+26
-16
@@ -19,6 +19,9 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
browserImage = "git.ipao.vip/rogee/fingerprint-chromium@sha256:b9f23b8e3ac640174db0dfa49e9095fe7eb06f5db55a4e7550d979b35ff3a1b7"
|
||||
browserUser = "1000:1000"
|
||||
browserEntrypoint = "/usr/local/bin/docker-entrypoint.sh"
|
||||
managedLabel = "io.creatorhub.managed"
|
||||
idLabel = "io.creatorhub.runtime-id"
|
||||
networkRoleLabel = "io.creatorhub.network-role"
|
||||
@@ -30,6 +33,8 @@ const (
|
||||
|
||||
var runtimeIDPattern = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{0,31}$`)
|
||||
var networkNamePattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$`)
|
||||
var errInvalidRuntimeID = errors.New("invalid runtime id")
|
||||
var errUnmanagedContainer = errors.New("refusing to operate on a container not owned by CreatorHub")
|
||||
|
||||
type config struct {
|
||||
image string
|
||||
@@ -61,11 +66,6 @@ type browser struct {
|
||||
}
|
||||
|
||||
func main() {
|
||||
image := os.Getenv("BROWSER_IMAGE")
|
||||
if image == "" || strings.HasSuffix(image, ":latest") || !strings.Contains(image, ":") {
|
||||
log.Fatal("BROWSER_IMAGE must be a pinned image tag")
|
||||
}
|
||||
|
||||
transport := &http.Transport{
|
||||
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
|
||||
return (&net.Dialer{}).DialContext(ctx, "unix", env("DOCKER_SOCKET", "/var/run/docker.sock"))
|
||||
@@ -79,7 +79,7 @@ func main() {
|
||||
if err := docker.ensureInternalNetwork(network); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
handler := newGateway(docker, config{image: image, network: network})
|
||||
handler := newGateway(docker, config{image: browserImage, network: network})
|
||||
|
||||
server := &http.Server{
|
||||
Addr: env("LISTEN_ADDR", ":8081"),
|
||||
@@ -161,9 +161,11 @@ func (api gateway) create(response http.ResponseWriter, request *http.Request) {
|
||||
|
||||
pidsLimit := int64(512)
|
||||
payload := map[string]any{
|
||||
"Image": api.config.image,
|
||||
"Cmd": []string{"--fingerprint=" + strconv.FormatInt(input.Seed, 10), "about:blank"},
|
||||
"Env": []string{"REMOTE_DEBUGGING_PORT=9222"},
|
||||
"Image": api.config.image,
|
||||
"User": browserUser,
|
||||
"Entrypoint": []string{browserEntrypoint},
|
||||
"Cmd": []string{"--fingerprint=" + strconv.FormatInt(input.Seed, 10), "about:blank"},
|
||||
"Env": []string{"REMOTE_DEBUGGING_PORT=9222"},
|
||||
"Labels": map[string]string{
|
||||
managedLabel: "true",
|
||||
idLabel: input.Name,
|
||||
@@ -208,7 +210,12 @@ func (api gateway) create(response http.ResponseWriter, request *http.Request) {
|
||||
return
|
||||
}
|
||||
if err := api.docker.expect(http.MethodPost, "/containers/"+url.PathEscape(created.ID)+"/start", nil, http.StatusNoContent, http.StatusNotModified); err != nil {
|
||||
writeError(response, http.StatusBadGateway, fmt.Errorf("container created but did not start: %w", err))
|
||||
cleanupErr := api.docker.expect(http.MethodDelete, "/containers/"+url.PathEscape(created.ID)+"?force=1&v=0", nil, http.StatusNoContent)
|
||||
if cleanupErr != nil {
|
||||
writeError(response, http.StatusBadGateway, fmt.Errorf("container did not start and cleanup failed: %w; cleanup: %v", err, cleanupErr))
|
||||
return
|
||||
}
|
||||
writeError(response, http.StatusBadGateway, fmt.Errorf("container did not start and was removed while preserving its Profile volume: %w", err))
|
||||
return
|
||||
}
|
||||
writeJSON(response, http.StatusCreated, map[string]string{"id": created.ID, "name": input.Name})
|
||||
@@ -254,7 +261,7 @@ func (api gateway) remove(response http.ResponseWriter, request *http.Request) {
|
||||
|
||||
func (api gateway) requireManaged(id string) error {
|
||||
if !runtimeIDPattern.MatchString(id) {
|
||||
return errors.New("invalid runtime id")
|
||||
return errInvalidRuntimeID
|
||||
}
|
||||
result, err := api.docker.request(http.MethodGet, "/containers/"+url.PathEscape(namePrefix+id)+"/json", nil)
|
||||
if err != nil {
|
||||
@@ -276,7 +283,7 @@ func (api gateway) requireManaged(id string) error {
|
||||
return fmt.Errorf("decode Docker inspect: %w", err)
|
||||
}
|
||||
if inspected.Config.Labels[managedLabel] != "true" || inspected.Config.Labels[idLabel] != id {
|
||||
return errors.New("refusing to operate on a container not owned by CreatorHub")
|
||||
return errUnmanagedContainer
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -364,13 +371,16 @@ func (docker dockerClient) ensureInternalNetwork(name string) error {
|
||||
}
|
||||
|
||||
func statusFor(err error) int {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
switch {
|
||||
case errors.Is(err, errInvalidRuntimeID):
|
||||
return http.StatusBadRequest
|
||||
case errors.Is(err, os.ErrNotExist):
|
||||
return http.StatusNotFound
|
||||
}
|
||||
if strings.Contains(err.Error(), "refusing") {
|
||||
case errors.Is(err, errUnmanagedContainer):
|
||||
return http.StatusForbidden
|
||||
default:
|
||||
return http.StatusBadGateway
|
||||
}
|
||||
return http.StatusBadRequest
|
||||
}
|
||||
|
||||
func forwardDockerError(response http.ResponseWriter, result *http.Response) {
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestGatewayCreatesConstrainedBrowser(t *testing.T) {
|
||||
@@ -43,6 +49,9 @@ func TestGatewayCreatesConstrainedBrowser(t *testing.T) {
|
||||
if created["Image"] != "registry.example/browser:1.2.3" {
|
||||
t.Fatalf("gateway accepted an unexpected image: %#v", created["Image"])
|
||||
}
|
||||
if created["User"] != browserUser || created["Entrypoint"].([]any)[0] != browserEntrypoint {
|
||||
t.Fatalf("runtime identity is not fixed: user=%#v entrypoint=%#v", created["User"], created["Entrypoint"])
|
||||
}
|
||||
host := created["HostConfig"].(map[string]any)
|
||||
if host["NetworkMode"] != "creatorhub_browser" || host["ReadonlyRootfs"] != true {
|
||||
t.Fatalf("missing container isolation: %#v", host)
|
||||
@@ -57,6 +66,101 @@ func TestGatewayCreatesConstrainedBrowser(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGatewayRemovesFailedContainerAndPreservesProfile(t *testing.T) {
|
||||
removed := false
|
||||
docker := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
|
||||
switch {
|
||||
case request.Method == http.MethodPost && request.URL.Path == "/containers/create":
|
||||
response.WriteHeader(http.StatusCreated)
|
||||
_, _ = response.Write([]byte(`{"Id":"failed-id"}`))
|
||||
case request.Method == http.MethodPost && request.URL.Path == "/containers/failed-id/start":
|
||||
http.Error(response, "start failed", http.StatusInternalServerError)
|
||||
case request.Method == http.MethodDelete && request.URL.Path == "/containers/failed-id":
|
||||
removed = request.URL.Query().Get("force") == "1" && request.URL.Query().Get("v") == "0"
|
||||
response.WriteHeader(http.StatusNoContent)
|
||||
default:
|
||||
t.Fatalf("unexpected Docker request %s %s", request.Method, request.URL.String())
|
||||
}
|
||||
}))
|
||||
defer docker.Close()
|
||||
|
||||
handler := newGateway(dockerClient{baseURL: docker.URL, client: docker.Client()}, config{image: browserImage, network: "creatorhub_browser"})
|
||||
request := httptest.NewRequest(http.MethodPost, "/v1/browsers", strings.NewReader(`{"name":"account-a","seed":1000}`))
|
||||
response := httptest.NewRecorder()
|
||||
handler.ServeHTTP(response, request)
|
||||
|
||||
if response.Code != http.StatusBadGateway || !removed {
|
||||
t.Fatalf("expected failed container cleanup with preserved volume, status=%d removed=%v body=%s", response.Code, removed, response.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestGatewayListsBrowsers(t *testing.T) {
|
||||
docker := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
|
||||
if request.Method != http.MethodGet || request.URL.Path != "/containers/json" {
|
||||
t.Fatalf("unexpected Docker request %s %s", request.Method, request.URL.String())
|
||||
}
|
||||
_, _ = response.Write([]byte(`[{"Id":"container-id","State":"running","Status":"Up","Labels":{"io.creatorhub.runtime-id":"account-a"}}]`))
|
||||
}))
|
||||
defer docker.Close()
|
||||
|
||||
handler := newGateway(dockerClient{baseURL: docker.URL, client: docker.Client()}, config{})
|
||||
response := httptest.NewRecorder()
|
||||
handler.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/v1/browsers", nil))
|
||||
|
||||
var browsers []browser
|
||||
if response.Code != http.StatusOK || json.NewDecoder(response.Body).Decode(&browsers) != nil || len(browsers) != 1 || browsers[0].Name != "account-a" {
|
||||
t.Fatalf("unexpected list response status=%d body=%s", response.Code, response.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestGatewayLifecycle(t *testing.T) {
|
||||
tests := []struct {
|
||||
method string
|
||||
path string
|
||||
dockerPath string
|
||||
}{
|
||||
{http.MethodPost, "/v1/browsers/account-a/start", "/containers/creatorhub-browser-account-a/start"},
|
||||
{http.MethodPost, "/v1/browsers/account-a/stop", "/containers/creatorhub-browser-account-a/stop"},
|
||||
{http.MethodDelete, "/v1/browsers/account-a", "/containers/creatorhub-browser-account-a"},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.method+" "+test.path, func(t *testing.T) {
|
||||
docker := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
|
||||
if request.Method == http.MethodGet {
|
||||
_, _ = response.Write([]byte(`{"Config":{"Labels":{"io.creatorhub.managed":"true","io.creatorhub.runtime-id":"account-a"}}}`))
|
||||
return
|
||||
}
|
||||
if request.URL.Path != test.dockerPath {
|
||||
t.Fatalf("unexpected Docker path %s", request.URL.String())
|
||||
}
|
||||
response.WriteHeader(http.StatusNoContent)
|
||||
}))
|
||||
defer docker.Close()
|
||||
|
||||
handler := newGateway(dockerClient{baseURL: docker.URL, client: docker.Client()}, config{})
|
||||
response := httptest.NewRecorder()
|
||||
handler.ServeHTTP(response, httptest.NewRequest(test.method, test.path, nil))
|
||||
if response.Code != http.StatusNoContent {
|
||||
t.Fatalf("expected 204, got %d: %s", response.Code, response.Body.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGatewayMapsDockerServiceFailureToBadGateway(t *testing.T) {
|
||||
docker := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, _ *http.Request) {
|
||||
http.Error(response, "daemon unavailable", http.StatusInternalServerError)
|
||||
}))
|
||||
defer docker.Close()
|
||||
|
||||
handler := newGateway(dockerClient{baseURL: docker.URL, client: docker.Client()}, config{})
|
||||
response := httptest.NewRecorder()
|
||||
handler.ServeHTTP(response, httptest.NewRequest(http.MethodDelete, "/v1/browsers/account-a", nil))
|
||||
if response.Code != http.StatusBadGateway {
|
||||
t.Fatalf("expected 502 for Docker failure, got %d: %s", response.Code, response.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestGatewayRefusesUnmanagedContainer(t *testing.T) {
|
||||
deleted := false
|
||||
docker := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
|
||||
@@ -183,3 +287,46 @@ func TestEnsureInternalNetworkRejectsControlNetwork(t *testing.T) {
|
||||
t.Fatalf("expected control network to be rejected before Docker request, requested=%v err=%v", requested, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDockerIntegrationBlocksCrossContainerTraffic(t *testing.T) {
|
||||
if os.Getenv("CREATORHUB_DOCKER_INTEGRATION") != "1" {
|
||||
t.Skip("set CREATORHUB_DOCKER_INTEGRATION=1 to run")
|
||||
}
|
||||
image := "alpine:3.22@sha256:14358309a308569c32bdc37e2e0e9694be33a9d99e68afb0f5ff33cc1f695dce"
|
||||
networkName := fmt.Sprintf("creatorhub_test_%d", time.Now().UnixNano())
|
||||
serverName := networkName + "_server"
|
||||
runDocker := func(args ...string) ([]byte, error) {
|
||||
return exec.Command("docker", args...).CombinedOutput()
|
||||
}
|
||||
if output, err := runDocker("pull", image); err != nil {
|
||||
t.Fatalf("pull integration image: %v: %s", err, output)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if output, err := runDocker("network", "rm", networkName); err != nil {
|
||||
t.Logf("remove test network: %v: %s", err, output)
|
||||
}
|
||||
})
|
||||
t.Cleanup(func() {
|
||||
if output, err := runDocker("rm", "-f", serverName); err != nil {
|
||||
t.Logf("remove test container: %v: %s", err, output)
|
||||
}
|
||||
})
|
||||
|
||||
transport := &http.Transport{DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
|
||||
return (&net.Dialer{}).DialContext(ctx, "unix", "/var/run/docker.sock")
|
||||
}}
|
||||
defer transport.CloseIdleConnections()
|
||||
client := dockerClient{baseURL: "http://docker/v1.43", client: &http.Client{Transport: transport}}
|
||||
if err := client.ensureInternalNetwork(networkName); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if output, err := runDocker("run", "-d", "--name", serverName, "--network", networkName, "--network-alias", "peer", image, "nc", "-lk", "-p", "8080", "-e", "/bin/echo"); err != nil {
|
||||
t.Fatalf("start peer server: %v: %s", err, output)
|
||||
}
|
||||
if output, err := runDocker("exec", serverName, "nc", "-z", "-w", "2", "127.0.0.1", "8080"); err != nil {
|
||||
t.Fatalf("peer server is not ready: %v: %s", err, output)
|
||||
}
|
||||
if output, err := runDocker("run", "--rm", "--network", networkName, image, "nc", "-z", "-w", "2", "peer", "8080"); err == nil {
|
||||
t.Fatalf("cross-container request unexpectedly succeeded: %s", output)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ services:
|
||||
build: .
|
||||
command: ["/app/docker-gateway"]
|
||||
environment:
|
||||
BROWSER_IMAGE: ${BROWSER_IMAGE:-git.ipao.vip/rogee/fingerprint-chromium:148.0.7778.215}
|
||||
BROWSER_NETWORK: creatorhub_browser
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
|
||||
@@ -27,17 +27,19 @@ React ── /api/browsers ──> control-plane ── /v1/browsers ──> doc
|
||||
- 启停和删除前必须同时匹配固定名称前缀及 `io.creatorhub.managed`、`io.creatorhub.runtime-id` 标签;
|
||||
- 动态容器使用只读根文件系统、非 root 镜像、全部 capability drop、`no-new-privileges`、CPU/内存/PID 限制,且无宿主机端口和目录挂载;
|
||||
- 控制面只绑定 `127.0.0.1`,控制网络为固定名称的 Compose internal 网络;浏览器 bridge 关闭 ICC,阻止运行时直接互访,并按 ownership、role、driver、Internal 和完整 options 失败关闭校验,且拒绝复用 control 网络;
|
||||
- `BROWSER_IMAGE` 必须是固定 tag,拒绝 `latest`。
|
||||
- 浏览器镜像固定为审核过的 immutable digest;运行时创建请求同时固定非 root `1000:1000` 与镜像入口,构建阶段和最终基础镜像也均锁定 digest。
|
||||
|
||||
网关自身一旦被攻破,socket 仍允许接管宿主机;应用内校验不能消除这个平台级风险。进入共享或生产环境前,应把浏览器调度迁到独立 Docker daemon/VM,或使用宿主机 Docker authorization plugin 做第二层强制授权,不应把 socket 暴露给公网服务。
|
||||
网关自身一旦被攻破,socket 仍允许接管宿主机;应用内校验不能消除这个平台级风险。当前控制面没有认证或 CSRF 防护,只支持可信单用户从 localhost 访问。进入共享或生产环境前,必须先补认证与 CSRF 边界,并把浏览器调度迁到独立 Docker daemon/VM,或使用宿主机 Docker authorization plugin 做第二层强制授权,不应把 socket 暴露给公网服务。
|
||||
|
||||
## 运行
|
||||
|
||||
Docker socket 的 GID 因宿主机而异:
|
||||
|
||||
```bash
|
||||
docker pull git.ipao.vip/rogee/fingerprint-chromium:148.0.7778.215
|
||||
docker pull git.ipao.vip/rogee/fingerprint-chromium@sha256:b9f23b8e3ac640174db0dfa49e9095fe7eb06f5db55a4e7550d979b35ff3a1b7
|
||||
DOCKER_GID=$(stat -c %g /var/run/docker.sock) docker compose up --build
|
||||
```
|
||||
|
||||
打开 <http://127.0.0.1:8080>。默认浏览器镜像固定为 `git.ipao.vip/rogee/fingerprint-chromium:148.0.7778.215`,可通过 `BROWSER_IMAGE` 改为其他已审核的固定 tag。阶段 A 网络默认失败关闭且不能访问外网;后续授权沙箱必须先设计受管出口,再调整 `browser` 网络。
|
||||
打开 <http://127.0.0.1:8080>。浏览器镜像固定为 `git.ipao.vip/rogee/fingerprint-chromium@sha256:b9f23b8e3ac640174db0dfa49e9095fe7eb06f5db55a4e7550d979b35ff3a1b7`。阶段 A 网络默认失败关闭且不能访问外网;后续授权沙箱必须先设计受管出口,再调整 `browser` 网络。
|
||||
|
||||
创建成功但启动失败时,网关会立即删除失败容器并保留命名 Profile 卷,允许同名请求安全重试。跨容器隔离回归可用 `CREATORHUB_DOCKER_INTEGRATION=1 go test -run TestDockerIntegrationBlocksCrossContainerTraffic ./cmd/docker-gateway` 运行。
|
||||
|
||||
Reference in New Issue
Block a user