mirror of
https://github.com/drone-plugins/drone-docker.git
synced 2026-07-16 16:21:18 +08:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 48f8235649 | |||
| d10b5ea8b8 | |||
| 8371ac46ae | |||
| a4a38753ce | |||
| 1ade22dbc0 |
@@ -804,6 +804,74 @@ pipeline:
|
||||
- acr
|
||||
buildIntelligence:
|
||||
enabled: false
|
||||
- stage:
|
||||
name: win-ltsc-2025-amd64
|
||||
identifier: winltsc2025amd64
|
||||
description: ""
|
||||
type: CI
|
||||
spec:
|
||||
cloneCodebase: true
|
||||
caching:
|
||||
enabled: false
|
||||
override: false
|
||||
paths: []
|
||||
buildIntelligence:
|
||||
enabled: false
|
||||
infrastructure:
|
||||
type: VM
|
||||
spec:
|
||||
type: Pool
|
||||
spec:
|
||||
poolName: windows-2025
|
||||
os: Windows
|
||||
execution:
|
||||
steps:
|
||||
- step:
|
||||
type: Run
|
||||
name: Build Binary 2025
|
||||
identifier: Build_Binary_2025
|
||||
spec:
|
||||
connectorRef: Plugins_Docker_Hub_Connector
|
||||
image: golang:1.23.0
|
||||
shell: Sh
|
||||
command: |-
|
||||
# disable cgo
|
||||
export CGO_ENABLED=0
|
||||
|
||||
go build -o release/windows/amd64/drone-<+matrix.repo>.exe ./cmd/drone-<+matrix.repo>
|
||||
strategy:
|
||||
matrix:
|
||||
repo:
|
||||
- docker
|
||||
- gcr
|
||||
- gar
|
||||
- ecr
|
||||
- acr
|
||||
- step:
|
||||
type: Plugin
|
||||
name: Plugin_1
|
||||
identifier: Plugin_1
|
||||
spec:
|
||||
connectorRef: Plugins_Docker_Hub_Connector
|
||||
image: plugins/docker:windows-ltsc2025-amd64
|
||||
settings:
|
||||
username: drone
|
||||
password: <+secrets.getValue("Plugins_Docker_Hub_Pat")>
|
||||
repo: plugins/<+matrix.repo>
|
||||
dockerfile: docker/<+matrix.repo>/Dockerfile.windows.amd64.ltsc2025
|
||||
auto_tag: "true"
|
||||
auto_tag_suffix: windows-ltsc2025-amd64
|
||||
strategy:
|
||||
matrix:
|
||||
repo:
|
||||
- docker
|
||||
- gcr
|
||||
- gar
|
||||
- ecr
|
||||
- acr
|
||||
when:
|
||||
stageStatus: Success
|
||||
condition: <+codebase.build.type> == "tag"
|
||||
- stage:
|
||||
name: Manifest and Release
|
||||
identifier: Manifest
|
||||
|
||||
@@ -114,6 +114,54 @@ COOL BANANAS
|
||||
```
|
||||
|
||||
|
||||
### Trusting a custom / egress-proxy CA (`HARNESS_CA_PATH`)
|
||||
|
||||
When builds run behind a TLS-intercepting egress proxy (e.g. Harness egress
|
||||
control), the proxy re-signs upstream TLS with its own CA. The Docker daemon and
|
||||
its embedded BuildKit verify registry TLS against the **host system trust
|
||||
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**:
|
||||
|
||||
```yaml
|
||||
steps:
|
||||
- name: build and push
|
||||
image: plugins/docker
|
||||
pull: never
|
||||
environment:
|
||||
HARNESS_CA_PATH: /etc/harness-certs/ca.crt
|
||||
settings:
|
||||
repo: octocat/hello-world
|
||||
tags: latest
|
||||
```
|
||||
|
||||
Behavior:
|
||||
|
||||
- **Linux** — installs into the distro anchor dir
|
||||
(`/usr/local/share/ca-certificates` on Debian/Ubuntu/Alpine,
|
||||
`/etc/pki/ca-trust/source/anchors` on RHEL/CentOS/Fedora) and runs
|
||||
`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`.
|
||||
- **macOS / other** — logged no-op (not currently supported).
|
||||
|
||||
Notes:
|
||||
|
||||
- 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.
|
||||
- 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).
|
||||
|
||||
### Running from the CLI
|
||||
|
||||
```console
|
||||
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
//go:build linux
|
||||
// +build linux
|
||||
|
||||
package docker
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// anchorCandidate is a distro CA anchor directory plus the tool that refreshes
|
||||
// the consolidated bundle from it.
|
||||
type anchorCandidate struct {
|
||||
dir string
|
||||
refresh []string
|
||||
}
|
||||
|
||||
// linuxAnchorCandidates and linuxBundleCandidates are package vars (not consts)
|
||||
// so tests can point them at a temp trust store. Order matters: the first
|
||||
// existing entry wins for the anchor install.
|
||||
var linuxAnchorCandidates = []anchorCandidate{
|
||||
{"/usr/local/share/ca-certificates", []string{"update-ca-certificates"}}, // Debian/Ubuntu/Alpine
|
||||
{"/etc/pki/ca-trust/source/anchors", []string{"update-ca-trust", "extract"}}, // RHEL/CentOS/Fedora
|
||||
}
|
||||
|
||||
var linuxBundleCandidates = []string{
|
||||
"/etc/ssl/certs/ca-certificates.crt", // Debian/Ubuntu/Alpine
|
||||
"/etc/pki/tls/certs/ca-bundle.crt", // RHEL/CentOS
|
||||
}
|
||||
|
||||
// installHarnessCA drops the CA into the distro anchor directory and refreshes
|
||||
// the system bundle. It also appends directly to the consolidated bundle so
|
||||
// trust is effective even on images that lack update-ca-certificates (e.g.
|
||||
// minimal distros); the Docker daemon and Go's crypto/x509 read from that
|
||||
// bundle. Best-effort: failures are logged, never fatal.
|
||||
func installHarnessCA(caPEM []byte) {
|
||||
anchorCandidates := linuxAnchorCandidates
|
||||
|
||||
installedViaAnchor := false
|
||||
for _, c := range anchorCandidates {
|
||||
if _, err := os.Stat(c.dir); err != nil {
|
||||
continue
|
||||
}
|
||||
dst := filepath.Join(c.dir, "harness-egress-ca.crt")
|
||||
if err := os.WriteFile(dst, caPEM, 0644); err != nil {
|
||||
fmt.Printf("Could not write Harness CA to %s: %s\n", dst, err)
|
||||
continue
|
||||
}
|
||||
if _, err := exec.LookPath(c.refresh[0]); err != nil {
|
||||
continue
|
||||
}
|
||||
cmd := exec.Command(c.refresh[0], c.refresh[1:]...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
fmt.Printf("Warning: %s failed: %s\n", c.refresh[0], err)
|
||||
continue
|
||||
}
|
||||
fmt.Printf("Installed Harness egress CA into %s and refreshed system trust store.\n", c.dir)
|
||||
installedViaAnchor = true
|
||||
break
|
||||
}
|
||||
|
||||
// Belt-and-suspenders: append to the consolidated bundle(s) directly so the
|
||||
// CA is trusted even when no refresh tool exists or the anchor step failed.
|
||||
bundleCandidates := linuxBundleCandidates
|
||||
appended := false
|
||||
for _, bundle := range bundleCandidates {
|
||||
if _, err := os.Stat(bundle); err != nil {
|
||||
continue
|
||||
}
|
||||
if bundleContainsCA(bundle, caPEM) {
|
||||
appended = true
|
||||
continue
|
||||
}
|
||||
f, err := os.OpenFile(bundle, os.O_APPEND|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
fmt.Printf("Could not append Harness CA to %s: %s\n", bundle, err)
|
||||
continue
|
||||
}
|
||||
payload := caPEM
|
||||
if !bytes.HasPrefix(payload, []byte("\n")) {
|
||||
payload = append([]byte("\n"), payload...)
|
||||
}
|
||||
if _, err := f.Write(payload); err != nil {
|
||||
fmt.Printf("Could not append Harness CA to %s: %s\n", bundle, err)
|
||||
} else {
|
||||
fmt.Printf("Appended Harness egress CA to %s.\n", bundle)
|
||||
appended = true
|
||||
}
|
||||
f.Close()
|
||||
}
|
||||
|
||||
if !installedViaAnchor && !appended {
|
||||
fmt.Println("Warning: could not install the Harness egress CA into any known trust store location; base-image pulls through the egress proxy may fail with x509 errors.")
|
||||
}
|
||||
}
|
||||
|
||||
// bundleContainsCA reports whether the CA bytes are already present in the
|
||||
// bundle, to keep the step idempotent across retries.
|
||||
func bundleContainsCA(bundle string, caPEM []byte) bool {
|
||||
existing, err := os.ReadFile(bundle)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return bytes.Contains(existing, bytes.TrimSpace(caPEM))
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
//go:build linux
|
||||
// +build linux
|
||||
|
||||
package docker
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// withTempTrustStore points the Linux anchor/bundle candidates at a temp dir so
|
||||
// installHarnessCA can be exercised without touching the real system store.
|
||||
func withTempTrustStore(t *testing.T) (anchorDir, bundlePath string) {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
anchorDir = filepath.Join(dir, "anchors")
|
||||
bundlePath = filepath.Join(dir, "ca-certificates.crt")
|
||||
if err := os.MkdirAll(anchorDir, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Seed an existing bundle so the append path is exercised.
|
||||
if err := os.WriteFile(bundlePath, []byte("# existing roots\n"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
origAnchors := linuxAnchorCandidates
|
||||
origBundles := linuxBundleCandidates
|
||||
// No refresh tool on the anchor entry so the test doesn't shell out.
|
||||
linuxAnchorCandidates = []anchorCandidate{{dir: anchorDir, refresh: []string{"harness-nonexistent-refresh-tool"}}}
|
||||
linuxBundleCandidates = []string{bundlePath}
|
||||
t.Cleanup(func() {
|
||||
linuxAnchorCandidates = origAnchors
|
||||
linuxBundleCandidates = origBundles
|
||||
})
|
||||
return anchorDir, bundlePath
|
||||
}
|
||||
|
||||
func TestInstallHarnessCA_Linux(t *testing.T) {
|
||||
anchorDir, bundlePath := withTempTrustStore(t)
|
||||
|
||||
installHarnessCA([]byte(testCAPEM))
|
||||
|
||||
// Anchor file should be written.
|
||||
anchorFile := filepath.Join(anchorDir, "harness-egress-ca.crt")
|
||||
got, err := os.ReadFile(anchorFile)
|
||||
if err != nil {
|
||||
t.Fatalf("expected anchor file at %s: %s", anchorFile, err)
|
||||
}
|
||||
if string(got) != testCAPEM {
|
||||
t.Fatalf("anchor file = %q, want %q", string(got), testCAPEM)
|
||||
}
|
||||
|
||||
// Bundle should have the CA appended (refresh tool is absent, so the
|
||||
// direct-append fallback is what provides trust here).
|
||||
bundle, err := os.ReadFile(bundlePath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(string(bundle), strings.TrimSpace(testCAPEM)) {
|
||||
t.Fatalf("bundle %q does not contain appended CA", string(bundle))
|
||||
}
|
||||
if !strings.HasPrefix(string(bundle), "# existing roots") {
|
||||
t.Fatalf("bundle append clobbered existing roots: %q", string(bundle))
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstallHarnessCA_Linux_Idempotent(t *testing.T) {
|
||||
_, bundlePath := withTempTrustStore(t)
|
||||
|
||||
installHarnessCA([]byte(testCAPEM))
|
||||
installHarnessCA([]byte(testCAPEM))
|
||||
|
||||
bundle, err := os.ReadFile(bundlePath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if n := strings.Count(string(bundle), strings.TrimSpace(testCAPEM)); n != 1 {
|
||||
t.Fatalf("expected CA to appear exactly once after two installs, got %d", n)
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
//go:build !linux && !windows
|
||||
// +build !linux,!windows
|
||||
|
||||
package docker
|
||||
|
||||
import "fmt"
|
||||
|
||||
// installHarnessCA is a no-op on platforms where egress control is not yet
|
||||
// supported (e.g. macOS). HARNESS_CA_PATH is honored on Linux and Windows.
|
||||
func installHarnessCA(caPEM []byte) {
|
||||
fmt.Println("HARNESS_CA_PATH is set but egress-proxy CA trust injection is not supported on this platform; skipping.")
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// decodePEMCertificates extracts DER-encoded CERTIFICATE blocks from a PEM
|
||||
// (or PEM bundle). Non-certificate blocks are skipped. Returns an error when
|
||||
// no certificate blocks are present.
|
||||
func decodePEMCertificates(pemBytes []byte) ([][]byte, error) {
|
||||
var ders [][]byte
|
||||
rest := pemBytes
|
||||
for {
|
||||
var block *pem.Block
|
||||
block, rest = pem.Decode(rest)
|
||||
if block == nil {
|
||||
break
|
||||
}
|
||||
if block.Type != "CERTIFICATE" {
|
||||
continue
|
||||
}
|
||||
ders = append(ders, block.Bytes)
|
||||
}
|
||||
if len(ders) == 0 {
|
||||
return nil, fmt.Errorf("no CERTIFICATE PEM blocks found")
|
||||
}
|
||||
return ders, nil
|
||||
}
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Real self-signed CAs generated for these tests (CN=HarnessTestCA / HarnessTestCA2).
|
||||
const harnessTestCAPEM = `-----BEGIN CERTIFICATE-----
|
||||
MIIDETCCAfmgAwIBAgIUUnMVT6JwuvsR8lPanhZxYJnqUPYwDQYJKoZIhvcNAQEL
|
||||
BQAwGDEWMBQGA1UEAwwNSGFybmVzc1Rlc3RDQTAeFw0yNjA3MTUwNzMxMDBaFw0z
|
||||
NjA3MTIwNzMxMDBaMBgxFjAUBgNVBAMMDUhhcm5lc3NUZXN0Q0EwggEiMA0GCSqG
|
||||
SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDJOssgNxmGP8itq7IxObLY2igTK3/YeMIJ
|
||||
9gvjWzUGe/RRrWqWJvfplKScNaIbyGaABYbDaGURnL3uGr5QQWEUbqvOMlEYaoNq
|
||||
21TmVbEl6Sponc+4aFfDPP9CFAOn2tqyrZaU3wZIjSUWbGo2wwseWULzaQxZ8lZ6
|
||||
a+kgjipymolfTZeHbJQAk/hwXVhbrz6xBbOhue/K/tFJxFBuawvwgXEi+2ywzXgy
|
||||
fn80wYWdjeFEpFAtNKyjg+bu36DtHikwDS9XqZzgSZnGhDKEGh3cj0hd3/nhr59h
|
||||
FCDm56XsBvWQVHmNmiuuxCn3svzSa9qdyk6tcIqZN8wKz1J7lFRfAgMBAAGjUzBR
|
||||
MB0GA1UdDgQWBBSrLR7fG/bwSozrDipyfT5MtiuYjTAfBgNVHSMEGDAWgBSrLR7f
|
||||
G/bwSozrDipyfT5MtiuYjTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUA
|
||||
A4IBAQAq9+CxKqe2vhBTz+5i9NBpxzxFy3fZTKcakuB98hafnNw29D4VggjV7sgo
|
||||
99En87gkTIPsiOsRa0YH3wl5aa5EUp/Dx0ZfXasjzS5BFSZKZDYcuLuuUEeK9N6j
|
||||
UBxaWDxNRjENevkX8SWpbxyNkcDGZM2IfJvVA/g7h8ERZLjlu9iOTqb6jt3PDe4q
|
||||
CNoMbU5L3md3gk8mfoEZAX/IIUmbw7hAy7zE9dEfbKHeXl3FrcXPL0WmLePcK9BF
|
||||
8XEpsOUSoq4sMPdVxaPF7l+SKDbdsR4gKtwlEMLefiiLqk0k4HhygjzzRgdsMOc3
|
||||
S8dL7zv0BrAFy//XsfmsVeVFVhRN
|
||||
-----END CERTIFICATE-----
|
||||
`
|
||||
|
||||
const harnessTestCA2PEM = `-----BEGIN CERTIFICATE-----
|
||||
MIIDEzCCAfugAwIBAgIUR5AfE7MTe0ucAk1JzlqvOJd3Z7AwDQYJKoZIhvcNAQEL
|
||||
BQAwGTEXMBUGA1UEAwwOSGFybmVzc1Rlc3RDQTIwHhcNMjYwNzE1MDczMTAwWhcN
|
||||
MzYwNzEyMDczMTAwWjAZMRcwFQYDVQQDDA5IYXJuZXNzVGVzdENBMjCCASIwDQYJ
|
||||
KoZIhvcNAQEBBQADggEPADCCAQoCggEBAI2EDM2Ac+jwhP8OaWFFb+DlVwzKSkEI
|
||||
LoaZ41RQGHZaCckTLtaCtIBaNQHLWqj8q6I0fIz+8mmaXuxRKDjnhGl3AFIJ2wql
|
||||
Hbyywo2MQcu+6wfs2Ewd887NDb5wrNJs/gl1GYRilSAd8n53T5IJUHHA0IGxtq4g
|
||||
9XwS6OTmfBdQ3ycEsi9Yexd5Oz79sLMDhBnt21nYWrEO8Kumgyfd7gfQUBcGc1nH
|
||||
NhoBrZffLmo+cljGqPNEFyAlWEnsmiqZnYh/EOUXGdXxsZMvyMWt02kLe0BlGmJr
|
||||
PrI8iJCMElj86UhKK2oIjWLMA6cviYSkw9jyiihhodSUChhmqOABoe8CAwEAAaNT
|
||||
MFEwHQYDVR0OBBYEFKK9RP95NSP4xmxN+hojPXlxVVatMB8GA1UdIwQYMBaAFKK9
|
||||
RP95NSP4xmxN+hojPXlxVVatMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL
|
||||
BQADggEBABlrORwXmhmO+eIdTHZMuad72JMEJeHxiRCGbwH6vAVb03YZAXLmIf31
|
||||
sBKkUCDV+Vm0MaTYmYncK3eZZBn6teEWcx7+L9XlF7ckVKBqj9xkwFUdfVipCimD
|
||||
cjBl/H+2PEY5Hzoy/uf2/sE1czrfHA2HeEIEacVyht9t0HTtqXQqrn44ijESkNtb
|
||||
SqqaVYCcceS85ID2oJYYKQycMnFJd0VwSHlD+Lu4ZubXbrg+pzSqw6olTOwCE7Q2
|
||||
LhLxQRB4f8Xw807aIuFK2mKbPs+4dmPkhClM3rbFzzldQJ4lKLnvZHcFtXJUXg2a
|
||||
UzwP8JQn8xuz7gIZxMO3vGqTweP48yc=
|
||||
-----END CERTIFICATE-----
|
||||
`
|
||||
|
||||
func TestDecodePEMCertificates_Single(t *testing.T) {
|
||||
ders, err := decodePEMCertificates([]byte(harnessTestCAPEM))
|
||||
if err != nil {
|
||||
t.Fatalf("decodePEMCertificates: %v", err)
|
||||
}
|
||||
if len(ders) != 1 {
|
||||
t.Fatalf("got %d certs, want 1", len(ders))
|
||||
}
|
||||
cert, err := x509.ParseCertificate(ders[0])
|
||||
if err != nil {
|
||||
t.Fatalf("ParseCertificate: %v", err)
|
||||
}
|
||||
if cert.Subject.CommonName != "HarnessTestCA" {
|
||||
t.Fatalf("CN = %q, want HarnessTestCA", cert.Subject.CommonName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodePEMCertificates_Bundle(t *testing.T) {
|
||||
bundle := harnessTestCAPEM + "\n" + harnessTestCA2PEM
|
||||
ders, err := decodePEMCertificates([]byte(bundle))
|
||||
if err != nil {
|
||||
t.Fatalf("decodePEMCertificates: %v", err)
|
||||
}
|
||||
if len(ders) != 2 {
|
||||
t.Fatalf("got %d certs, want 2", len(ders))
|
||||
}
|
||||
for i, der := range ders {
|
||||
if _, err := x509.ParseCertificate(der); err != nil {
|
||||
t.Fatalf("cert %d: ParseCertificate: %v", i, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodePEMCertificates_SkipsNonCertificateBlocks(t *testing.T) {
|
||||
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: []byte("not-a-key")})
|
||||
mixed := append(append([]byte{}, keyPEM...), []byte(harnessTestCAPEM)...)
|
||||
ders, err := decodePEMCertificates(mixed)
|
||||
if err != nil {
|
||||
t.Fatalf("decodePEMCertificates: %v", err)
|
||||
}
|
||||
if len(ders) != 1 {
|
||||
t.Fatalf("got %d certs, want 1 (private key block skipped)", len(ders))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodePEMCertificates_Empty(t *testing.T) {
|
||||
_, err := decodePEMCertificates([]byte("not a pem\n"))
|
||||
if err == nil {
|
||||
t.Fatal("expected error for input with no CERTIFICATE blocks")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodePEMCertificates_MatchesOpenSSLDER(t *testing.T) {
|
||||
ders, err := decodePEMCertificates([]byte(harnessTestCAPEM))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
origBlock, _ := pem.Decode([]byte(harnessTestCAPEM))
|
||||
if !bytes.Equal(origBlock.Bytes, ders[0]) {
|
||||
t.Fatal("decoded DER does not match original PEM payload")
|
||||
}
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const testCAPEM = `-----BEGIN CERTIFICATE-----
|
||||
MIIBAg==
|
||||
-----END CERTIFICATE-----
|
||||
`
|
||||
|
||||
// withStubbedInstall replaces the platform installer with a capturing stub for
|
||||
// the duration of a test, returning a pointer to the captured CA bytes and a
|
||||
// pointer to a call counter.
|
||||
func withStubbedInstall(t *testing.T) (captured *[]byte, calls *int) {
|
||||
t.Helper()
|
||||
captured = new([]byte)
|
||||
calls = new(int)
|
||||
orig := installHarnessCAFn
|
||||
installHarnessCAFn = func(caPEM []byte) {
|
||||
*calls++
|
||||
*captured = append([]byte(nil), caPEM...)
|
||||
}
|
||||
t.Cleanup(func() { installHarnessCAFn = orig })
|
||||
return captured, calls
|
||||
}
|
||||
|
||||
func TestTrustHarnessCA_Unset(t *testing.T) {
|
||||
os.Unsetenv("HARNESS_CA_PATH")
|
||||
_, calls := withStubbedInstall(t)
|
||||
|
||||
trustHarnessCA()
|
||||
|
||||
if *calls != 0 {
|
||||
t.Fatalf("expected installer not to be called when HARNESS_CA_PATH is unset, got %d calls", *calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrustHarnessCA_MissingFile(t *testing.T) {
|
||||
missing := filepath.Join(t.TempDir(), "does-not-exist.crt")
|
||||
os.Setenv("HARNESS_CA_PATH", missing)
|
||||
defer os.Unsetenv("HARNESS_CA_PATH")
|
||||
_, calls := withStubbedInstall(t)
|
||||
|
||||
trustHarnessCA()
|
||||
|
||||
if *calls != 0 {
|
||||
t.Fatalf("expected installer not to be called when CA file is missing, got %d calls", *calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrustHarnessCA_EmptyFile(t *testing.T) {
|
||||
empty := filepath.Join(t.TempDir(), "empty.crt")
|
||||
if err := os.WriteFile(empty, []byte(" \n\t"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
os.Setenv("HARNESS_CA_PATH", empty)
|
||||
defer os.Unsetenv("HARNESS_CA_PATH")
|
||||
_, calls := withStubbedInstall(t)
|
||||
|
||||
trustHarnessCA()
|
||||
|
||||
if *calls != 0 {
|
||||
t.Fatalf("expected installer not to be called for whitespace-only CA, got %d calls", *calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrustHarnessCA_ValidFile(t *testing.T) {
|
||||
valid := filepath.Join(t.TempDir(), "ca.crt")
|
||||
if err := os.WriteFile(valid, []byte(testCAPEM), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
os.Setenv("HARNESS_CA_PATH", valid)
|
||||
defer os.Unsetenv("HARNESS_CA_PATH")
|
||||
captured, calls := withStubbedInstall(t)
|
||||
|
||||
trustHarnessCA()
|
||||
|
||||
if *calls != 1 {
|
||||
t.Fatalf("expected installer to be called once for a valid CA, got %d calls", *calls)
|
||||
}
|
||||
if string(*captured) != testCAPEM {
|
||||
t.Fatalf("installer received %q, want %q", string(*captured), testCAPEM)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package docker
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
// installHarnessCA imports the Harness egress-proxy CA into the Windows
|
||||
// certificate store under LocalMachine\Root (the machine-wide trusted roots the
|
||||
// Docker daemon and Go's crypto/x509 consult). Best-effort: failures are logged,
|
||||
// never fatal.
|
||||
//
|
||||
// Uses CryptoAPI (crypt32.dll) directly so it works on Nano Server images that
|
||||
// lack certutil and PowerShell. CERT_STORE_ADD_REPLACE_EXISTING keeps the step
|
||||
// idempotent across retries.
|
||||
func installHarnessCA(caPEM []byte) {
|
||||
ders, err := decodePEMCertificates(caPEM)
|
||||
if err != nil {
|
||||
fmt.Printf("Could not decode Harness CA PEM: %s\n", err)
|
||||
fmt.Println("Base-image pulls through the egress proxy may fail with x509 errors.")
|
||||
return
|
||||
}
|
||||
|
||||
storeName, err := windows.UTF16PtrFromString("ROOT")
|
||||
if err != nil {
|
||||
fmt.Printf("Could not prepare Windows root store name: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
store, err := windows.CertOpenStore(
|
||||
windows.CERT_STORE_PROV_SYSTEM,
|
||||
0,
|
||||
0,
|
||||
windows.CERT_SYSTEM_STORE_LOCAL_MACHINE,
|
||||
uintptr(unsafe.Pointer(storeName)),
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Printf("Could not open LocalMachine\\Root store: %s\n", err)
|
||||
fmt.Println("Base-image pulls through the egress proxy may fail with x509 errors.")
|
||||
return
|
||||
}
|
||||
defer windows.CertCloseStore(store, 0)
|
||||
|
||||
for i, der := range ders {
|
||||
if err := addCertToStore(store, der); err != nil {
|
||||
fmt.Printf("Warning: failed to add Harness CA cert %d to LocalMachine\\Root: %s\n", i+1, err)
|
||||
fmt.Println("Base-image pulls through the egress proxy may fail with x509 errors.")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("Installed %d Harness egress CA certificate(s) into the Windows LocalMachine\\Root store.\n", len(ders))
|
||||
}
|
||||
|
||||
func addCertToStore(store windows.Handle, der []byte) error {
|
||||
if len(der) == 0 {
|
||||
return fmt.Errorf("empty DER certificate")
|
||||
}
|
||||
ctx, err := windows.CertCreateCertificateContext(
|
||||
windows.X509_ASN_ENCODING|windows.PKCS_7_ASN_ENCODING,
|
||||
&der[0],
|
||||
uint32(len(der)),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("CertCreateCertificateContext: %w", err)
|
||||
}
|
||||
defer windows.CertFreeCertificateContext(ctx)
|
||||
|
||||
if err := windows.CertAddCertificateContextToStore(
|
||||
store,
|
||||
ctx,
|
||||
windows.CERT_STORE_ADD_REPLACE_EXISTING,
|
||||
nil,
|
||||
); err != nil {
|
||||
return fmt.Errorf("CertAddCertificateContextToStore: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
@@ -131,6 +132,14 @@ 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()
|
||||
@@ -331,7 +340,7 @@ func (p Plugin) Exec() error {
|
||||
|
||||
if digest, err := getDigest(p.Build.TempTag); err == nil {
|
||||
fmt.Printf("🔐 Found image digest: %s\n", digest)
|
||||
|
||||
|
||||
// Sign with digest reference
|
||||
imageRef := fmt.Sprintf("%s@%s", p.Build.Repo, digest)
|
||||
cosignCmd := createCosignCommand(imageRef, p.Cosign)
|
||||
@@ -339,7 +348,7 @@ func (p Plugin) Exec() error {
|
||||
} else {
|
||||
fmt.Printf("⚠️ WARNING: Could not get image digest for cosign signing: %s\n", err)
|
||||
fmt.Printf(" Falling back to tag-based signing\n")
|
||||
|
||||
|
||||
// Fall back to tag-based signing for each tag
|
||||
for _, tag := range p.Build.Tags {
|
||||
imageRef := fmt.Sprintf("%s:%s", p.Build.Repo, tag)
|
||||
@@ -574,6 +583,35 @@ 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.
|
||||
// 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() {
|
||||
caPath := os.Getenv("HARNESS_CA_PATH")
|
||||
if caPath == "" {
|
||||
return
|
||||
}
|
||||
|
||||
caPEM, err := os.ReadFile(caPath)
|
||||
if err != nil {
|
||||
fmt.Printf("HARNESS_CA_PATH is set to %q but the CA could not be read: %s. Skipping CA trust.\n", caPath, err)
|
||||
return
|
||||
}
|
||||
if len(bytes.TrimSpace(caPEM)) == 0 {
|
||||
fmt.Printf("HARNESS_CA_PATH %q is empty. Skipping CA trust.\n", caPath)
|
||||
return
|
||||
}
|
||||
|
||||
installHarnessCAFn(caPEM)
|
||||
}
|
||||
|
||||
// helper function to add proxy values from the environment
|
||||
func addProxyBuildArgs(build *Build) {
|
||||
addProxyValue(build, "http_proxy")
|
||||
@@ -853,7 +891,7 @@ func isValidPEMKey(pemContent string) bool {
|
||||
// createCosignCommand creates a cosign sign command with the given image reference
|
||||
func createCosignCommand(imageRef string, cosign CosignConfig) *exec.Cmd {
|
||||
args := []string{"sign", "--yes"}
|
||||
|
||||
|
||||
// Handle private key (content vs file path)
|
||||
if strings.HasPrefix(cosign.PrivateKey, "-----BEGIN") {
|
||||
args = append(args, "--key", "env://COSIGN_PRIVATE_KEY")
|
||||
@@ -861,21 +899,21 @@ func createCosignCommand(imageRef string, cosign CosignConfig) *exec.Cmd {
|
||||
} else {
|
||||
args = append(args, "--key", cosign.PrivateKey)
|
||||
}
|
||||
|
||||
|
||||
// Set password if provided
|
||||
if cosign.Password != "" {
|
||||
os.Setenv("COSIGN_PASSWORD", cosign.Password)
|
||||
}
|
||||
|
||||
|
||||
// Add any extra parameters
|
||||
if cosign.Params != "" {
|
||||
extraArgs := strings.Fields(cosign.Params)
|
||||
args = append(args, extraArgs...)
|
||||
}
|
||||
|
||||
|
||||
// Add the image reference to sign
|
||||
args = append(args, imageRef)
|
||||
|
||||
|
||||
return exec.Command(cosignExe, args...)
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ require (
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/urfave/cli v1.22.2
|
||||
golang.org/x/oauth2 v0.34.0
|
||||
golang.org/x/sys v0.39.0
|
||||
google.golang.org/api v0.187.0
|
||||
)
|
||||
|
||||
@@ -61,7 +62,6 @@ require (
|
||||
go.opentelemetry.io/otel/trace v1.39.0 // indirect
|
||||
golang.org/x/crypto v0.46.0 // indirect
|
||||
golang.org/x/net v0.48.0 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
||||
google.golang.org/grpc v1.79.3 // indirect
|
||||
|
||||
Reference in New Issue
Block a user