From d64f4c49aa8ece368137088f064137b8774385bc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Dec 2025 04:02:38 +0000 Subject: [PATCH 1/8] Initial plan From c1cc25c1b505119d4ff630b908de404b149bea5b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Dec 2025 04:09:06 +0000 Subject: [PATCH 2/8] Add Unix socket support for pkimetal integration - Add Socket field to PKIMetalConfig struct - Modify execute() to create HTTP client with Unix socket transport - Update docker-compose.yml to use pkimetal v1.32.0 with Unix socket - Configure socket volume sharing between boulder and pkimetal - Update test configs to use Unix socket instead of HTTP endpoint Co-authored-by: mcpherrinm <47425+mcpherrinm@users.noreply.github.com> --- docker-compose.yml | 9 +++++- linter/lints/rfc/lint_cert_via_pkimetal.go | 34 ++++++++++++++++++---- linter/lints/rfc/lint_crl_via_pkimetal.go | 4 +-- test/config-next/zlint.toml | 4 +-- test/config/zlint.toml | 4 +-- 5 files changed, 42 insertions(+), 13 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index d683f856837..a553f292876 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,6 +20,7 @@ services: - .:/boulder:cached - ./.gocache:/root/.cache/go-build:cached - ./test/certs/.softhsm-tokens/:/var/lib/softhsm/tokens/:cached + - pkimetal-socket:/var/run/pkimetal networks: bouldernet: ipv4_address: 10.77.77.77 @@ -141,7 +142,10 @@ services: - bouldernet bpkimetal: - image: ghcr.io/pkimetal/pkimetal:v1.20.0 + image: ghcr.io/pkimetal/pkimetal:v1.32.0 + command: ["--socket", "/var/run/pkimetal/pkimetal.sock"] + volumes: + - pkimetal-socket:/var/run/pkimetal networks: - bouldernet @@ -208,3 +212,6 @@ networks: driver: default config: - subnet: 64.112.117.128/25 + +volumes: + pkimetal-socket: diff --git a/linter/lints/rfc/lint_cert_via_pkimetal.go b/linter/lints/rfc/lint_cert_via_pkimetal.go index 31fc08d8135..5ad495dd651 100644 --- a/linter/lints/rfc/lint_cert_via_pkimetal.go +++ b/linter/lints/rfc/lint_cert_via_pkimetal.go @@ -6,6 +6,7 @@ import ( "encoding/json" "fmt" "io" + "net" "net/http" "net/url" "slices" @@ -21,6 +22,7 @@ import ( // both certs and CRLs using PKIMetal. type PKIMetalConfig struct { Addr string `toml:"addr" comment:"The address where a pkilint REST API can be reached."` + Socket string `toml:"socket" comment:"The Unix socket path where a pkilint REST API can be reached. If set, this takes precedence over Addr."` Severity string `toml:"severity" comment:"The minimum severity of findings to report (meta, debug, info, notice, warning, error, bug, or fatal)."` Timeout time.Duration `toml:"timeout" comment:"How long, in nanoseconds, to wait before giving up."` IgnoreLints []string `toml:"ignore_lints" comment:"The unique Validator:Code IDs of lint findings which should be ignored."` @@ -35,9 +37,29 @@ func (pkim *PKIMetalConfig) execute(endpoint string, der []byte) (*lint.LintResu ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() - apiURL, err := url.JoinPath(pkim.Addr, endpoint) - if err != nil { - return nil, fmt.Errorf("constructing pkimetal url: %w", err) + // Create HTTP client based on whether we're using Unix socket or HTTP + var client *http.Client + var apiURL string + var err error + + if pkim.Socket != "" { + // Use Unix socket connection + client = &http.Client{ + Transport: &http.Transport{ + DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { + return (&net.Dialer{}).DialContext(ctx, "unix", pkim.Socket) + }, + }, + } + // For Unix sockets, we use a dummy HTTP URL as the socket path is used for connection + apiURL = fmt.Sprintf("http://localhost/%s", endpoint) + } else { + // Use regular HTTP connection + client = http.DefaultClient + apiURL, err = url.JoinPath(pkim.Addr, endpoint) + if err != nil { + return nil, fmt.Errorf("constructing pkimetal url: %w", err) + } } // reqForm matches PKIMetal's documented form-urlencoded request format. It @@ -56,7 +78,7 @@ func (pkim *PKIMetalConfig) execute(endpoint string, der []byte) (*lint.LintResu req.Header.Add("Content-Type", "application/x-www-form-urlencoded") req.Header.Add("Accept", "application/json") - resp, err := http.DefaultClient.Do(req) + resp, err := client.Do(req) if err != nil { return nil, fmt.Errorf("making POST request to pkimetal API: %s (timeout %s)", err, timeout) } @@ -141,8 +163,8 @@ func (l *certViaPKIMetal) Configure() any { func (l *certViaPKIMetal) CheckApplies(c *x509.Certificate) bool { // This lint applies to all certificates issued by Boulder, as long as it has - // been configured with an address to reach out to. If not, skip it. - return l.Addr != "" + // been configured with an address or socket to reach out to. If not, skip it. + return l.Addr != "" || l.Socket != "" } func (l *certViaPKIMetal) Execute(c *x509.Certificate) *lint.LintResult { diff --git a/linter/lints/rfc/lint_crl_via_pkimetal.go b/linter/lints/rfc/lint_crl_via_pkimetal.go index c927eebe525..73ef163b81f 100644 --- a/linter/lints/rfc/lint_crl_via_pkimetal.go +++ b/linter/lints/rfc/lint_crl_via_pkimetal.go @@ -33,8 +33,8 @@ func (l *crlViaPKIMetal) Configure() any { func (l *crlViaPKIMetal) CheckApplies(c *x509.RevocationList) bool { // This lint applies to all CRLs issued by Boulder, as long as it has - // been configured with an address to reach out to. If not, skip it. - return l.Addr != "" + // been configured with an address or socket to reach out to. If not, skip it. + return l.Addr != "" || l.Socket != "" } func (l *crlViaPKIMetal) Execute(c *x509.RevocationList) *lint.LintResult { diff --git a/test/config-next/zlint.toml b/test/config-next/zlint.toml index b80dad07803..5460767a7b0 100644 --- a/test/config-next/zlint.toml +++ b/test/config-next/zlint.toml @@ -1,5 +1,5 @@ [e_pkimetal_lint_cabf_serverauth_cert] -addr = "http://bpkimetal:8080" +socket = "/var/run/pkimetal/pkimetal.sock" severity = "notice" timeout = 2000000000 # 2 seconds ignore_lints = [ @@ -22,7 +22,7 @@ ignore_lints = [ ] [e_pkimetal_lint_cabf_serverauth_crl] -addr = "http://bpkimetal:8080" +socket = "/var/run/pkimetal/pkimetal.sock" severity = "notice" timeout = 2000000000 # 2 seconds ignore_lints = [] diff --git a/test/config/zlint.toml b/test/config/zlint.toml index b044d1d3436..31b28a06457 100644 --- a/test/config/zlint.toml +++ b/test/config/zlint.toml @@ -1,5 +1,5 @@ [e_pkimetal_lint_cabf_serverauth_cert] -addr = "http://bpkimetal:8080" +socket = "/var/run/pkimetal/pkimetal.sock" severity = "notice" timeout = 2000000000 # 2 seconds ignore_lints = [ @@ -18,7 +18,7 @@ ignore_lints = [ ] [e_pkimetal_lint_cabf_serverauth_crl] -addr = "http://bpkimetal:8080" +socket = "/var/run/pkimetal/pkimetal.sock" severity = "notice" timeout = 2000000000 # 2 seconds ignore_lints = [] From cd0be2aa0f7b3f3723da6c637d912933e333e28d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Dec 2025 04:11:52 +0000 Subject: [PATCH 3/8] Address code review feedback for Unix socket implementation - Add timeout to HTTP client for Unix socket transport - Use url.JoinPath for Unix socket URL construction for consistency Co-authored-by: mcpherrinm <47425+mcpherrinm@users.noreply.github.com> --- linter/lints/rfc/lint_cert_via_pkimetal.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/linter/lints/rfc/lint_cert_via_pkimetal.go b/linter/lints/rfc/lint_cert_via_pkimetal.go index 5ad495dd651..011fef03726 100644 --- a/linter/lints/rfc/lint_cert_via_pkimetal.go +++ b/linter/lints/rfc/lint_cert_via_pkimetal.go @@ -45,6 +45,7 @@ func (pkim *PKIMetalConfig) execute(endpoint string, der []byte) (*lint.LintResu if pkim.Socket != "" { // Use Unix socket connection client = &http.Client{ + Timeout: timeout, Transport: &http.Transport{ DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { return (&net.Dialer{}).DialContext(ctx, "unix", pkim.Socket) @@ -52,7 +53,10 @@ func (pkim *PKIMetalConfig) execute(endpoint string, der []byte) (*lint.LintResu }, } // For Unix sockets, we use a dummy HTTP URL as the socket path is used for connection - apiURL = fmt.Sprintf("http://localhost/%s", endpoint) + apiURL, err = url.JoinPath("http://localhost", endpoint) + if err != nil { + return nil, fmt.Errorf("constructing pkimetal url: %w", err) + } } else { // Use regular HTTP connection client = http.DefaultClient From 3bedb7478d5ec9017fb72e1dcf764ba97d5bff95 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Dec 2025 04:16:09 +0000 Subject: [PATCH 4/8] Configure pkimetal with Viper YAML config for Unix socket - Create pkimetal.yaml config files with server.webserverPath setting - Mount config file in docker-compose instead of using command-line args - Add config for both test/config and test/config-next directories Co-authored-by: mcpherrinm <47425+mcpherrinm@users.noreply.github.com> --- docker-compose.yml | 2 +- test/config-next/pkimetal.yaml | 2 ++ test/config/pkimetal.yaml | 2 ++ 3 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 test/config-next/pkimetal.yaml create mode 100644 test/config/pkimetal.yaml diff --git a/docker-compose.yml b/docker-compose.yml index a553f292876..3ee8122477f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -143,9 +143,9 @@ services: bpkimetal: image: ghcr.io/pkimetal/pkimetal:v1.32.0 - command: ["--socket", "/var/run/pkimetal/pkimetal.sock"] volumes: - pkimetal-socket:/var/run/pkimetal + - ./test/config/pkimetal.yaml:/config.yaml:ro networks: - bouldernet diff --git a/test/config-next/pkimetal.yaml b/test/config-next/pkimetal.yaml new file mode 100644 index 00000000000..071985a2c17 --- /dev/null +++ b/test/config-next/pkimetal.yaml @@ -0,0 +1,2 @@ +server: + webserverPath: /var/run/pkimetal/pkimetal.sock diff --git a/test/config/pkimetal.yaml b/test/config/pkimetal.yaml new file mode 100644 index 00000000000..071985a2c17 --- /dev/null +++ b/test/config/pkimetal.yaml @@ -0,0 +1,2 @@ +server: + webserverPath: /var/run/pkimetal/pkimetal.sock From 847b6e7db987f79a0904480b30a23e940f1e74b9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Dec 2025 04:18:06 +0000 Subject: [PATCH 5/8] Set timeout on HTTP client for regular HTTP connections - Ensure both Unix socket and HTTP connections respect configured timeout - Improve comment clarity about localhost usage in Unix socket URLs Co-authored-by: mcpherrinm <47425+mcpherrinm@users.noreply.github.com> --- linter/lints/rfc/lint_cert_via_pkimetal.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/linter/lints/rfc/lint_cert_via_pkimetal.go b/linter/lints/rfc/lint_cert_via_pkimetal.go index 011fef03726..4241b1014c2 100644 --- a/linter/lints/rfc/lint_cert_via_pkimetal.go +++ b/linter/lints/rfc/lint_cert_via_pkimetal.go @@ -52,14 +52,17 @@ func (pkim *PKIMetalConfig) execute(endpoint string, der []byte) (*lint.LintResu }, }, } - // For Unix sockets, we use a dummy HTTP URL as the socket path is used for connection + // For Unix sockets, we use a dummy HTTP URL. The hostname is ignored since + // the custom DialContext overrides the connection mechanism to use the socket path. apiURL, err = url.JoinPath("http://localhost", endpoint) if err != nil { return nil, fmt.Errorf("constructing pkimetal url: %w", err) } } else { // Use regular HTTP connection - client = http.DefaultClient + client = &http.Client{ + Timeout: timeout, + } apiURL, err = url.JoinPath(pkim.Addr, endpoint) if err != nil { return nil, fmt.Errorf("constructing pkimetal url: %w", err) From 86c1eb929ef2feef31812dd9bc3025e19d5d8d3f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Dec 2025 04:32:56 +0000 Subject: [PATCH 6/8] Refactor HTTP client creation to enable connection reuse - Store http.Client in PKIMetalConfig struct for reuse across requests - Initialize client lazily on first execute() call - Only conditionally set Transport for Unix socket, simplifying code - Reduces verbosity by eliminating duplicate client creation logic Co-authored-by: mcpherrinm <47425+mcpherrinm@users.noreply.github.com> --- linter/lints/rfc/lint_cert_via_pkimetal.go | 56 +++++++++++----------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/linter/lints/rfc/lint_cert_via_pkimetal.go b/linter/lints/rfc/lint_cert_via_pkimetal.go index 4241b1014c2..7d8a0611907 100644 --- a/linter/lints/rfc/lint_cert_via_pkimetal.go +++ b/linter/lints/rfc/lint_cert_via_pkimetal.go @@ -26,6 +26,8 @@ type PKIMetalConfig struct { Severity string `toml:"severity" comment:"The minimum severity of findings to report (meta, debug, info, notice, warning, error, bug, or fatal)."` Timeout time.Duration `toml:"timeout" comment:"How long, in nanoseconds, to wait before giving up."` IgnoreLints []string `toml:"ignore_lints" comment:"The unique Validator:Code IDs of lint findings which should be ignored."` + + client *http.Client } func (pkim *PKIMetalConfig) execute(endpoint string, der []byte) (*lint.LintResult, error) { @@ -34,39 +36,37 @@ func (pkim *PKIMetalConfig) execute(endpoint string, der []byte) (*lint.LintResu timeout = 100 * time.Millisecond } - ctx, cancel := context.WithTimeout(context.Background(), timeout) - defer cancel() - - // Create HTTP client based on whether we're using Unix socket or HTTP - var client *http.Client - var apiURL string - var err error - - if pkim.Socket != "" { - // Use Unix socket connection - client = &http.Client{ + // Initialize HTTP client once and reuse it for connection pooling + if pkim.client == nil { + pkim.client = &http.Client{ Timeout: timeout, - Transport: &http.Transport{ + } + // If using Unix socket, set up custom transport + if pkim.Socket != "" { + pkim.client.Transport = &http.Transport{ DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { return (&net.Dialer{}).DialContext(ctx, "unix", pkim.Socket) }, - }, - } - // For Unix sockets, we use a dummy HTTP URL. The hostname is ignored since - // the custom DialContext overrides the connection mechanism to use the socket path. - apiURL, err = url.JoinPath("http://localhost", endpoint) - if err != nil { - return nil, fmt.Errorf("constructing pkimetal url: %w", err) + } } + } + + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + // Determine the base URL: use localhost for Unix sockets, otherwise use configured address + var baseURL string + if pkim.Socket != "" { + // For Unix sockets, the hostname is ignored since the custom DialContext + // overrides the connection mechanism to use the socket path. + baseURL = "http://localhost" } else { - // Use regular HTTP connection - client = &http.Client{ - Timeout: timeout, - } - apiURL, err = url.JoinPath(pkim.Addr, endpoint) - if err != nil { - return nil, fmt.Errorf("constructing pkimetal url: %w", err) - } + baseURL = pkim.Addr + } + + apiURL, err := url.JoinPath(baseURL, endpoint) + if err != nil { + return nil, fmt.Errorf("constructing pkimetal url: %w", err) } // reqForm matches PKIMetal's documented form-urlencoded request format. It @@ -85,7 +85,7 @@ func (pkim *PKIMetalConfig) execute(endpoint string, der []byte) (*lint.LintResu req.Header.Add("Content-Type", "application/x-www-form-urlencoded") req.Header.Add("Accept", "application/json") - resp, err := client.Do(req) + resp, err := pkim.client.Do(req) if err != nil { return nil, fmt.Errorf("making POST request to pkimetal API: %s (timeout %s)", err, timeout) } From 868ccc2737457d068792c90f0a26013d44f54142 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Dec 2025 04:35:31 +0000 Subject: [PATCH 7/8] Use sync.Once for thread-safe client initialization - Use sync.Once to ensure thread-safe HTTP client initialization - Remove timeout from client (apply per-request via context instead) - Prevents race conditions when multiple goroutines call execute() Co-authored-by: mcpherrinm <47425+mcpherrinm@users.noreply.github.com> --- linter/lints/rfc/lint_cert_via_pkimetal.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/linter/lints/rfc/lint_cert_via_pkimetal.go b/linter/lints/rfc/lint_cert_via_pkimetal.go index 7d8a0611907..fdbebc4f85b 100644 --- a/linter/lints/rfc/lint_cert_via_pkimetal.go +++ b/linter/lints/rfc/lint_cert_via_pkimetal.go @@ -11,6 +11,7 @@ import ( "net/url" "slices" "strings" + "sync" "time" "github.com/zmap/zcrypto/x509" @@ -27,7 +28,8 @@ type PKIMetalConfig struct { Timeout time.Duration `toml:"timeout" comment:"How long, in nanoseconds, to wait before giving up."` IgnoreLints []string `toml:"ignore_lints" comment:"The unique Validator:Code IDs of lint findings which should be ignored."` - client *http.Client + client *http.Client + clientOnce sync.Once } func (pkim *PKIMetalConfig) execute(endpoint string, der []byte) (*lint.LintResult, error) { @@ -36,11 +38,9 @@ func (pkim *PKIMetalConfig) execute(endpoint string, der []byte) (*lint.LintResu timeout = 100 * time.Millisecond } - // Initialize HTTP client once and reuse it for connection pooling - if pkim.client == nil { - pkim.client = &http.Client{ - Timeout: timeout, - } + // Initialize HTTP client once with thread-safe sync.Once for connection pooling + pkim.clientOnce.Do(func() { + pkim.client = &http.Client{} // If using Unix socket, set up custom transport if pkim.Socket != "" { pkim.client.Transport = &http.Transport{ @@ -49,7 +49,7 @@ func (pkim *PKIMetalConfig) execute(endpoint string, der []byte) (*lint.LintResu }, } } - } + }) ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() From e5a96b287507583cc3041abeacb1d86e7f3305ef Mon Sep 17 00:00:00 2001 From: Matthew McPherrin Date: Thu, 18 Dec 2025 20:19:34 -0500 Subject: [PATCH 8/8] Fix app path, run as root like the rest of boulder to fix permissions --- docker-compose.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 3ee8122477f..1d9236636d0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -143,9 +143,10 @@ services: bpkimetal: image: ghcr.io/pkimetal/pkimetal:v1.32.0 + user: root volumes: - pkimetal-socket:/var/run/pkimetal - - ./test/config/pkimetal.yaml:/config.yaml:ro + - ./test/config/pkimetal.yaml:/app/config.yaml:ro networks: - bouldernet