diff --git a/deploy/haproxy-ingress.yaml b/deploy/haproxy-ingress.yaml index 09a1463d..dfe2b31c 100644 --- a/deploy/haproxy-ingress.yaml +++ b/deploy/haproxy-ingress.yaml @@ -128,6 +128,7 @@ metadata: namespace: haproxy-controller data: ssl-redirect-port: "443" + generate-certificates: "true" --- apiVersion: apps/v1 diff --git a/documentation/annotations.md b/documentation/annotations.md index e1adf582..4fbf69d2 100644 --- a/documentation/annotations.md +++ b/documentation/annotations.md @@ -24,6 +24,8 @@ This is autogenerated from [doc.yaml](doc.yaml). Description can be found in [ge | [client-ca](#authentication) | string | | ssl-offloading |:large_blue_circle:|:white_circle:|:white_circle:| | [client-crt-optional](#authentication) | [bool](#bool) | "false" | client-ca |:large_blue_circle:|:white_circle:|:white_circle:| | [client-strict-sni](#ssl-offloading) | [bool](#bool) | "false" | client-ca |:large_blue_circle:|:white_circle:|:white_circle:| +| [generate-certificates](#ssl-offloading) | [bool](#bool) | "false" | |:large_blue_circle:|:white_circle:|:white_circle:| +| [ca-sign-file](#ssl-offloading) | string | | generate-certificates |:large_blue_circle:|:white_circle:|:white_circle:| | [cors-enable](#CORS) | [bool](#bool) | "false" | |:large_blue_circle:|:large_blue_circle:|:white_circle:| | [cors-allow-origin](#CORS) | string | "*" | cors-enable |:large_blue_circle:|:large_blue_circle:|:white_circle:| | [cors-allow-methods](#CORS) | string | "*" | cors-enable |:large_blue_circle:|:large_blue_circle:|:white_circle:| @@ -1698,6 +1700,52 @@ Example: client-strict-sni: true ``` +##### `generate-certificates` + + Enables automatic certificate generation on the HTTPS frontend bind line using HAProxy's generate-certificates feature. + This allows HAProxy to automatically generate certificates for incoming TLS connections. + + Available on: `configmap` + + :information_source: This requires HAProxy to be configured with a ca-sign-file for certificate generation to work. + + :information_source: The generate-certificates option is added to the bind line in the HTTPS frontend when SSL offload is enabled. + +Possible values: + +- true +- false `default` + +Example: + +```yaml +generate-certificates: true +``` + +##### `ca-sign-file` + + Specifies the Kubernetes secret containing the CA certificate file used to sign automatically generated certificates. + This annotation works in conjunction with generate-certificates to provide the signing CA for certificate generation. + The secret should contain the CA certificate in PEM format. + + Available on: `configmap` + + :information_source: This is only used when generate-certificates is enabled. + + :information_source: The secret format should be namespace/secret-name. + + :information_source: HAProxy will use this CA to sign certificates generated for incoming TLS connections. + +Possible values: + +- Name of Kubernetes secret in format namespace/secret-name + +Example: + +```yaml +ca-sign-file: "default/ca-signing-cert" +``` + ##### `ssl-certificate` Sets the name of the Kubernetes secret that contains both the TLS key and certificate. diff --git a/documentation/doc.yaml b/documentation/doc.yaml index 1f401147..2d786099 100644 --- a/documentation/doc.yaml +++ b/documentation/doc.yaml @@ -686,6 +686,45 @@ annotations: version_min: "1.8" example: - "client-strict-sni: true" + - title: generate-certificates + type: bool + group: ssl-offloading + dependencies: "" + default: "false" + description: + - Enables automatic certificate generation on the HTTPS frontend bind line using HAProxy's generate-certificates feature. + - This allows HAProxy to automatically generate certificates for incoming TLS connections. + tip: + - This requires HAProxy to be configured with a ca-sign-file for certificate generation to work. + - The generate-certificates option is added to the bind line in the HTTPS frontend when SSL offload is enabled. + values: + - "true" + - "false" + applies_to: + - configmap + version_min: "1.11" + example: + - "generate-certificates: true" + - title: ca-sign-file + type: string + group: ssl-offloading + dependencies: generate-certificates + default: "" + description: + - Specifies the Kubernetes kubernetes.io/tls type secret containing the CA certificate file used to sign automatically generated certificates. + - This annotation works in conjunction with generate-certificates to provide the signing CA for certificate generation. + - The secret should contain the CA certificate in PEM format. + tip: + - This is only used when generate-certificates is enabled. + - The secret format should be namespace/secret-name or just secret-name (uses controller namespace). + - HAProxy will use this CA to sign certificates generated for incoming TLS connections. + values: + - Name of Kubernetes secret in format namespace/secret-name + applies_to: + - configmap + version_min: "1.11" + example: + - 'ca-sign-file: "default/ca-signing-cert"' - title: cors-enable type: bool group: CORS diff --git a/pkg/annotations/common/main.go b/pkg/annotations/common/main.go index 6415c58b..bd7ec3ad 100644 --- a/pkg/annotations/common/main.go +++ b/pkg/annotations/common/main.go @@ -63,6 +63,8 @@ var DefaultValues = map[string]string{ "client-crt-optional": "false", "tls-alpn": "h2,http/1.1", "quic-alt-svc-max-age": "60", + "generate-certificates": "false", + "ca-sign-file": "", } // Returns the first annotation value in the set of maps of annotations along with the indice of which map in argument provided the value. diff --git a/pkg/handler/https.go b/pkg/handler/https.go index 6edcf5e5..08fe5026 100644 --- a/pkg/handler/https.go +++ b/pkg/handler/https.go @@ -34,15 +34,17 @@ import ( ) type HTTPS struct { - AddrIPv4 string - AddrIPv6 string - CertDir string - alpn string - Port int64 - Enabled bool - IPv4 bool - IPv6 bool - strictSNI bool + AddrIPv4 string + AddrIPv6 string + CertDir string + alpn string + Port int64 + Enabled bool + IPv4 bool + IPv6 bool + strictSNI bool + generateCertificates bool + caSignFile string } //nolint:golint, stylecheck @@ -170,11 +172,37 @@ func (handler *HTTPS) Update(k store.K8s, h haproxy.HAProxy, a annotations.Annot handler.strictSNI, err = annotations.Bool("client-strict-sni", k.ConfigMaps.Main.Annotations) logger.Error(err) + handler.generateCertificates, err = annotations.Bool("generate-certificates", k.ConfigMaps.Main.Annotations) + logger.Error(err) + + // Handle ca-sign-file secret for certificate generation + handler.caSignFile = "" + if handler.generateCertificates { + var notFound store.ErrNotFound + secret, annErr := annotations.Secret("ca-sign-file", "", k, k.ConfigMaps.Main.Annotations) + if annErr != nil { + if errors.Is(annErr, notFound) { + logger.Debugf("ca-sign-file not configured: %s", annErr) + } else { + err = fmt.Errorf("ca-sign-file: %w", annErr) + return err + } + } + if secret != nil { + caFile, certErr := h.Certificates.AddSecret(secret, certs.FT_CERT) + if certErr != nil { + err = fmt.Errorf("ca-sign-file: %w", certErr) + return err + } + handler.caSignFile = caFile + } + } + // ssl-offload sslOffloadEnabled := h.FrontendSSLOffloadEnabled(h.FrontHTTPS) if h.FrontCertsInUse() { if !sslOffloadEnabled { - logger.Panic(h.FrontendEnableSSLOffload(h.FrontHTTPS, handler.CertDir, handler.alpn, handler.strictSNI)) + logger.Panic(h.FrontendEnableSSLOffload(h.FrontHTTPS, handler.CertDir, handler.alpn, handler.strictSNI, handler.generateCertificates, handler.caSignFile)) instance.Reload("SSL offload enabled") } err := handler.handleClientTLSAuth(k, h) @@ -268,7 +296,7 @@ func (handler *HTTPS) toggleSSLPassthrough(passthrough bool, h haproxy.HAProxy) } } if h.FrontendSSLOffloadEnabled(h.FrontHTTPS) || h.FrontCertsInUse() { - logger.Panic(h.FrontendEnableSSLOffload(h.FrontHTTPS, handler.CertDir, handler.alpn, handler.strictSNI)) + logger.Panic(h.FrontendEnableSSLOffload(h.FrontHTTPS, handler.CertDir, handler.alpn, handler.strictSNI, handler.generateCertificates, handler.caSignFile)) } return nil } diff --git a/pkg/handler/tcp-services.go b/pkg/handler/tcp-services.go index fda7364e..09ce864e 100644 --- a/pkg/handler/tcp-services.go +++ b/pkg/handler/tcp-services.go @@ -160,7 +160,7 @@ func (handler TCPServices) createTCPFrontend(h haproxy.HAProxy, frontend models. })) } if sslOffload { - errors.Add(h.FrontendEnableSSLOffload(frontend.Name, handler.CertDir, "", false)) + errors.Add(h.FrontendEnableSSLOffload(frontend.Name, handler.CertDir, "", false, false, "")) } if errors.Result() != nil { err = fmt.Errorf("error configuring tcp frontend: %w", err) @@ -191,7 +191,7 @@ func (handler TCPServices) updateTCPFrontend(k store.K8s, h haproxy.HAProxy, fro return err } if !binds[0].Ssl && p.sslOffload { - err = h.FrontendEnableSSLOffload(frontend.Name, handler.CertDir, "", false) + err = h.FrontendEnableSSLOffload(frontend.Name, handler.CertDir, "", false, false, "") if err != nil { err = fmt.Errorf("failed to enable SSL offload: %w", err) return err diff --git a/pkg/haproxy/api/api.go b/pkg/haproxy/api/api.go index 8f502f62..3ec33cf8 100644 --- a/pkg/haproxy/api/api.go +++ b/pkg/haproxy/api/api.go @@ -66,7 +66,7 @@ type HAProxyClient interface { //nolint:interfacebloat FrontendsGet() (models.Frontends, error) FrontendGet(frontendName string) (models.Frontend, error) FrontendEdit(frontend models.FrontendBase) error - FrontendEnableSSLOffload(frontendName string, certDir string, alpn string, strictSNI bool) (err error) + FrontendEnableSSLOffload(frontendName string, certDir string, alpn string, strictSNI bool, generateCertificates bool, caSignFile string) (err error) FrontendDisableSSLOffload(frontendName string) (err error) FrontendSSLOffloadEnabled(frontendName string) bool Bind diff --git a/pkg/haproxy/api/frontend.go b/pkg/haproxy/api/frontend.go index 37040db5..27578b2c 100644 --- a/pkg/haproxy/api/frontend.go +++ b/pkg/haproxy/api/frontend.go @@ -73,7 +73,7 @@ func (c *clientNative) FrontendEdit(frontend models.FrontendBase) error { return configuration.EditFrontend(frontend.Name, f, c.activeTransaction, 0) } -func (c *clientNative) FrontendEnableSSLOffload(frontendName string, certDir string, alpn string, strictSNI bool) (err error) { +func (c *clientNative) FrontendEnableSSLOffload(frontendName string, certDir string, alpn string, strictSNI bool, generateCertificates bool, caSignFile string) (err error) { binds, err := c.FrontendBindsGet(frontendName) if err != nil { return err @@ -81,6 +81,10 @@ func (c *clientNative) FrontendEnableSSLOffload(frontendName string, certDir str for _, bind := range binds { bind.Ssl = true bind.SslCertificate = certDir + bind.GenerateCertificates = generateCertificates + if caSignFile != "" { + bind.CaSignFile = caSignFile + } if alpn != "" { bind.Alpn = alpn bind.StrictSni = strictSNI @@ -103,8 +107,10 @@ func (c *clientNative) FrontendDisableSSLOffload(frontendName string) (err error bind.SslCafile = "" bind.Verify = "" bind.SslCertificate = "" + bind.CaSignFile = "" bind.Alpn = "" bind.StrictSni = false + bind.GenerateCertificates = false err = c.FrontendBindEdit(frontendName, *bind) } if err != nil {