-
Notifications
You must be signed in to change notification settings - Fork 2.4k
HTTP-01 challenge: solver mishandles host headers containing an IPv6 address without a port number #8423
Description
Describe the bug:
The parseHost function in pkg/issuer/acme/http/solver/solver.go incorrectly parses the HTTP Host header containing an IPv6 address without a port number.
According to RFC 3986 (Section 3.2.2) and RFC 9110, an IPv6 address in the HTTP Host header must be enclosed in square brackets (e.g., [2a00:8a00:4000:435::13a]). Additionally, a port number may be assigned as needed (e.g., [2a00:8a00:4000:435::13a]:80).
The current implementation uses netip.ParseAddr (which fails on bracketed IP addresses without a port number) and falls back to a naive strings.Split(s, ":"). This logic incorrectly truncates the IPv6 address at the first colon, resulting in an invalid host string (e.g., [2a00).
By the way, parsing IPv6 address literals that include port numbers succeeds. ([2a00:8a00:4000:435::13a]:80 translates to 2a00:8a00:4000:435::13a.)
cert-manager/pkg/issuer/acme/http/solver/solver.go
Lines 119 to 134 in 6e38ee5
| func parseHost(s string) string { | |
| // ip v4/v6 with port | |
| addrPort, err := netip.ParseAddrPort(s) | |
| if err == nil { | |
| return addrPort.Addr().String() | |
| } | |
| // ip v4/v6 without port | |
| addr, err := netip.ParseAddr(s) | |
| if err == nil { | |
| return addr.String() | |
| } | |
| host := strings.Split(s, ":") | |
| return host[0] | |
| } |
This causes ACME HTTP-01 challenges to fail when traffic is routed via IPv6, as the challenge check compares a broken host string.
Expected behaviour:
The function should correctly extract the IPv6 address from the bracketed Host header (e.g., returning 2a00:8a00:4000:435::13a when the Host header is [2a00:8a00:4000:435::13a]), instead of truncating it.
Steps to reproduce the bug:
- Create
IssuerandCertificateresources with ahttp01resolver and IP address subjects. (For reference, Let's Encrypt has released short-lived IP address certificates into GA.)
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
name: example-issuer
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
profile: shortlived
solvers:
- http01:
ingress:
...
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: example-certificate
spec:
secretName: example-certificate-secret
ipAddresses:
- 2a00:8a00:4000:435::13a
issuerRef:
kind: Issuer
name: example-issuer- Observe the HTTP-01 challenges executed against the Pod created by cert-manager. Then the ACME HTTP-01 challenge is failing.
I0120 10:16:06.542507 1 solver.go:52] "starting listener" logger="cert-manager.acmesolver" expected_domain="2a00:8a00:4000:435::13a" expected_token="<token>" expected_key="<key>" listen_port=8089
I0120 10:16:15.483387 1 solver.go:89] "validating request" logger="cert-manager.acmesolver" host="[2a00" path="/.well-known/acme-challenge/<token>" base_path="/.well-known/acme-challenge" token="<token>" headers={"Accept-Encoding":["gzip"],"User-Agent":["cert-manager-challenges/v1.19.2 (linux/amd64) cert-manager/6e38ee57a338a1f27bb724ddb5933f4b8e23e567"]}
I0120 10:16:15.483446 1 solver.go:97] "comparing host" logger="cert-manager.acmesolver" host="[2a00" path="/.well-known/acme-challenge/<token>" base_path="/.well-known/acme-challenge" token="<token>" headers={"Accept-Encoding":["gzip"],"User-Agent":["cert-manager-challenges/v1.19.2 (linux/amd64) cert-manager/6e38ee57a338a1f27bb724ddb5933f4b8e23e567"]} expected_host="2a00:8a00:4000:435::13a"
I0120 10:16:15.483469 1 solver.go:99] "invalid host" logger="cert-manager.acmesolver" host="[2a00" path="/.well-known/acme-challenge/<token>" base_path="/.well-known/acme-challenge" token="<token>" headers={"Accept-Encoding":["gzip"],"User-Agent":["cert-manager-challenges/v1.19.2 (linux/amd64) cert-manager/6e38ee57a338a1f27bb724ddb5933f4b8e23e567"]} expected_host="2a00:8a00:4000:435::13a"
Anything else we need to know?:
Here is the minimal code that reproduces the bug.
package main
import (
"net/netip"
"strings"
)
func main() {
result := parseHost("[2a00:8a00:4000:435::13a]")
println(result) // Output: [2a00
}
func parseHost(s string) string {
// ip v4/v6 with port
addrPort, err := netip.ParseAddrPort(s)
if err == nil {
return addrPort.Addr().String()
}
// ip v4/v6 without port
addr, err := netip.ParseAddr(s)
if err == nil {
return addr.String()
}
host := strings.Split(s, ":")
return host[0]
}Environment details:
- Kubernetes version: v1.35.0
- Cloud-provider/provisioner: Bare metal, plain kubeadm installation
- cert-manager version: v1.19.2
- Install method: Helm chart (w/ v1.19.2)
/kind bug