diff --git a/README.md b/README.md index 542f6a3..3baa254 100644 --- a/README.md +++ b/README.md @@ -123,8 +123,8 @@ store**, so base-image pulls fail with `x509: certificate signed by unknown authority` unless that CA is trusted. Set the `HARNESS_CA_PATH` environment variable to a PEM CA (bundle) file and the -plugin installs it into the host trust store **before starting the Docker -daemon**: +plugin installs it into the host trust store **before any HTTPS** (registry +wrapper auth such as GCP STS, and before starting the Docker daemon): ```yaml steps: @@ -146,18 +146,22 @@ Behavior: `update-ca-certificates` / `update-ca-trust`. It also appends the CA directly to the consolidated bundle (`/etc/ssl/certs/ca-certificates.crt` or the RHEL equivalent) so trust holds even on minimal images without a refresh tool. -- **Windows** — imports the CA into the machine trust store via - `certutil -addstore -f Root`. +- **Windows** — imports the CA into `LocalMachine\Root` via the Windows + CryptoAPI (`crypt32.dll`). This works on Nano Server images that lack + `certutil` / PowerShell. - **macOS / other** — logged no-op (not currently supported). Notes: +- Registry wrappers (`plugins/gcr`, `gar`, `acr`, `ecr`) call the same install + before their pre-docker HTTPS auth (e.g. `sts.googleapis.com` token exchange). - It is **best-effort and idempotent**: when `HARNESS_CA_PATH` is unset, the file is missing, or the file is empty, the plugin logs and continues. Re-running the step will not duplicate the CA in the bundle. - The CA is trusted for the daemon's own registry TLS (base-image pulls, cache - endpoints). To make the CA available to `RUN` steps inside the build, also pass - it as needed via the Dockerfile / build args. + endpoints) and for Go TLS clients in the plugin process. To make the CA + available to `RUN` steps inside the build, also pass it as needed via the + Dockerfile / build args. - This is separate from the proxy build args (`http_proxy`/`https_proxy`/`no_proxy`), which the plugin already injects from the environment (including `HARNESS_`-prefixed variants). diff --git a/ca_test.go b/ca_test.go index d45e079..567833f 100644 --- a/ca_test.go +++ b/ca_test.go @@ -31,7 +31,7 @@ func TestTrustHarnessCA_Unset(t *testing.T) { os.Unsetenv("HARNESS_CA_PATH") _, calls := withStubbedInstall(t) - trustHarnessCA() + TrustHarnessCA() if *calls != 0 { t.Fatalf("expected installer not to be called when HARNESS_CA_PATH is unset, got %d calls", *calls) @@ -44,7 +44,7 @@ func TestTrustHarnessCA_MissingFile(t *testing.T) { defer os.Unsetenv("HARNESS_CA_PATH") _, calls := withStubbedInstall(t) - trustHarnessCA() + TrustHarnessCA() if *calls != 0 { t.Fatalf("expected installer not to be called when CA file is missing, got %d calls", *calls) @@ -60,7 +60,7 @@ func TestTrustHarnessCA_EmptyFile(t *testing.T) { defer os.Unsetenv("HARNESS_CA_PATH") _, calls := withStubbedInstall(t) - trustHarnessCA() + TrustHarnessCA() if *calls != 0 { t.Fatalf("expected installer not to be called for whitespace-only CA, got %d calls", *calls) @@ -76,7 +76,7 @@ func TestTrustHarnessCA_ValidFile(t *testing.T) { defer os.Unsetenv("HARNESS_CA_PATH") captured, calls := withStubbedInstall(t) - trustHarnessCA() + TrustHarnessCA() if *calls != 1 { t.Fatalf("expected installer to be called once for a valid CA, got %d calls", *calls) diff --git a/cmd/drone-acr/main.go b/cmd/drone-acr/main.go index 8dc884d..73feded 100644 --- a/cmd/drone-acr/main.go +++ b/cmd/drone-acr/main.go @@ -52,6 +52,10 @@ func main() { godotenv.Load(env) } + // Must run before Azure / ACR HTTPS auth: this binary does auth before + // spawning drone-docker, which is where TrustHarnessCA also runs for the daemon. + docker.TrustHarnessCA() + var ( repo = getenv("PLUGIN_REPO") registry = getenv("PLUGIN_REGISTRY") diff --git a/cmd/drone-docker/main.go b/cmd/drone-docker/main.go index 2a58dca..cdef83a 100644 --- a/cmd/drone-docker/main.go +++ b/cmd/drone-docker/main.go @@ -24,6 +24,10 @@ func main() { godotenv.Load(env) } + // Install egress CA before any TLS in this process (including the Docker + // daemon started from Plugin.Exec). + docker.TrustHarnessCA() + app := cli.NewApp() app.Name = "docker plugin" app.Usage = "docker plugin" diff --git a/cmd/drone-ecr/main.go b/cmd/drone-ecr/main.go index 6d4dac6..8474c71 100644 --- a/cmd/drone-ecr/main.go +++ b/cmd/drone-ecr/main.go @@ -30,6 +30,10 @@ func main() { godotenv.Load(env) } + // Must run before AWS / ECR HTTPS auth: this binary does auth before + // spawning drone-docker, which is where TrustHarnessCA also runs for the daemon. + docker.TrustHarnessCA() + var ( repo = getenv("PLUGIN_REPO") registry = getenv("PLUGIN_REGISTRY") diff --git a/cmd/drone-gar/main.go b/cmd/drone-gar/main.go index bd6ef46..e48928b 100644 --- a/cmd/drone-gar/main.go +++ b/cmd/drone-gar/main.go @@ -49,6 +49,10 @@ func loadConfig() Config { } } + // Must run before STS / oauth HTTPS: this binary does auth before spawning + // drone-docker, which is where TrustHarnessCA also runs for the daemon. + docker.TrustHarnessCA() + idToken := getenv("PLUGIN_OIDC_TOKEN_ID") projectId := getenv("PLUGIN_PROJECT_NUMBER") poolId := getenv("PLUGIN_POOL_ID") diff --git a/cmd/drone-gcr/main.go b/cmd/drone-gcr/main.go index 87e7cc4..0eb2bfc 100644 --- a/cmd/drone-gcr/main.go +++ b/cmd/drone-gcr/main.go @@ -39,6 +39,10 @@ func loadConfig() Config { } } + // Must run before STS / oauth HTTPS: this binary does auth before spawning + // drone-docker, which is where TrustHarnessCA also runs for the daemon. + docker.TrustHarnessCA() + idToken := getenv("PLUGIN_OIDC_TOKEN_ID") projectId := getenv("PLUGIN_PROJECT_NUMBER") poolId := getenv("PLUGIN_POOL_ID") diff --git a/docker.go b/docker.go index 6c07dec..6955b86 100644 --- a/docker.go +++ b/docker.go @@ -132,14 +132,6 @@ type ( // Exec executes the plugin step func (p Plugin) Exec() error { - // Trust the Harness egress-proxy CA (if HARNESS_CA_PATH is set) BEFORE the - // daemon starts. The Docker daemon and its embedded BuildKit verify registry - // TLS against the host system trust store; when traffic goes through the - // TLS-intercepting egress proxy, the presented cert is signed by the Harness - // CA and must be trusted here or base-image pulls fail with an x509 - // "unknown authority" error. - trustHarnessCA() - // start the Docker daemon server if !p.Daemon.Disabled { p.startDaemon() @@ -583,17 +575,23 @@ func getSecretCmdArg(kvp string, file bool) (string, error) { return fmt.Sprintf("id=%s,env=%s", key, value), nil } -// trustHarnessCA installs the Harness egress-proxy CA into the host trust store -// so the Docker daemon / embedded BuildKit trust the TLS-intercepting proxy when -// pulling base images. It is driven by HARNESS_CA_PATH (path to a PEM CA bundle) -// and is a best-effort no-op when the var is unset, the file is missing, or the -// platform isn't supported — build failures are surfaced later by docker itself. +// TrustHarnessCA installs the Harness egress-proxy CA into the host trust store +// so the Docker daemon / embedded BuildKit (and this process's Go TLS clients) +// trust the TLS-intercepting proxy. It is driven by HARNESS_CA_PATH (path to a +// PEM CA bundle) and is a best-effort no-op when the var is unset, the file is +// missing, or the platform isn't supported — build failures are surfaced later +// by docker itself. +// +// Call from each binary's main (after loading PLUGIN_ENV_FILE) before any HTTPS: +// Go caches SystemCertPool on first use, and registry wrappers (gcr/gar/acr/ecr) +// perform auth HTTPS before spawning drone-docker. +// // The platform-specific installation is provided by installHarnessCA (see // ca_linux.go / ca_windows.go / ca_other.go); it is indirected through // installHarnessCAFn so tests can stub the actual system-trust write. var installHarnessCAFn = installHarnessCA -func trustHarnessCA() { +func TrustHarnessCA() { caPath := os.Getenv("HARNESS_CA_PATH") if caPath == "" { return