Skip to content

Commit c997c81

Browse files
Merge pull request #19891 from ramr/mtls
Router support for mutual tls authentication
2 parents dd7fba2 + e789a67 commit c997c81

File tree

4 files changed

+185
-2
lines changed

4 files changed

+185
-2
lines changed

contrib/completions/bash/oc

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

contrib/completions/zsh/oc

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

images/router/haproxy/conf/haproxy-config.template

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,9 @@ frontend fe_sni
225225
{{- if isTrue (env "ROUTER_STRICT_SNI") }} strict-sni {{ end }}
226226
{{- ""}} crt {{firstMatch ".+" .DefaultCertificate "/var/lib/haproxy/conf/default_pub_keys.pem"}}
227227
{{- ""}} crt-list /var/lib/haproxy/conf/cert_config.map accept-proxy
228+
{{- with (env "ROUTER_MUTUAL_TLS_AUTH_CA") }} ca-file {{.}} {{ end }}
229+
{{- with (env "ROUTER_MUTUAL_TLS_AUTH_CRL") }} crl-file {{.}} {{ end }}
230+
{{- with (env "ROUTER_MUTUAL_TLS_AUTH") }} verify {{.}} {{ end }}
228231
{{- if isTrue (env "ROUTER_ENABLE_HTTP2") }} alpn h2,http/1.1{{ end }}
229232
mode http
230233

@@ -235,6 +238,37 @@ frontend fe_sni
235238
# before matching, or any requests containing uppercase characters will never match.
236239
http-request set-header Host %[req.hdr(Host),lower]
237240

241+
{{ if ne (env "ROUTER_MUTUAL_TLS_AUTH" "none") "none" }}
242+
{{- with (env "ROUTER_MUTUAL_TLS_AUTH_FILTER") }}
243+
# If a mutual TLS auth subject filter environment variable is set, we deny
244+
# requests if the DN field in the client certificate doesn't match that value.
245+
# Please note that this match is a regular expression match.
246+
# Example: For DN set to: /CN=header.test/ST=CA/C=US/O=Security/OU=OpenShift3,
247+
# A. ROUTER_MUTUAL_TLS_AUTH_FILTER="header.test" OR
248+
# ROUTER_MUTUAL_TLS_AUTH_FILTER="head" OR
249+
# ROUTER_MUTUAL_TLS_AUTH_FILTER="^/CN=header.test/ST=CA/C=US/O=Security/OU=OpenShift3$" /* exact match example */
250+
# the filter would match the DN field (substring or exact match)
251+
# and the request will be passed on to the backend.
252+
# B. ROUTER_MUTUAL_TLS_AUTH_FILTER="legacy-web-client", the request
253+
# will be rejected.
254+
acl cert_cn_matches ssl_c_s_dn -m reg {{.}}
255+
http-request deny unless cert_cn_matches
256+
{{- end }}
257+
258+
# Add X-SSL* headers to pass client certificate information to the backend.
259+
http-request set-header X-SSL %[ssl_fc]
260+
http-request set-header X-SSL-Client-Verify %[ssl_c_verify]
261+
http-request set-header X-SSL-Client-Serial %{+Q}[ssl_c_serial,hex]
262+
http-request set-header X-SSL-Client-Version %{+Q}[ssl_c_version]
263+
http-request set-header X-SSL-Client-SHA1 %{+Q}[ssl_c_sha1,hex]
264+
http-request set-header X-SSL-Client-DN %{+Q}[ssl_c_s_dn]
265+
http-request set-header X-SSL-Client-CN %{+Q}[ssl_c_s_dn(cn)]
266+
http-request set-header X-SSL-Issuer %{+Q}[ssl_c_i_dn]
267+
http-request set-header X-SSL-Client-NotBefore %{+Q}[ssl_c_notbefore]
268+
http-request set-header X-SSL-Client-NotAfter %{+Q}[ssl_c_notafter]
269+
http-request set-header X-SSL-Client-DER %{+Q}[ssl_c_der,base64]
270+
{{- end }}
271+
238272
# map to backend
239273
# Search from most specific to general path (host case).
240274
# Note: If no match, haproxy uses the default_backend, no other
@@ -261,6 +295,9 @@ backend be_no_sni
261295
frontend fe_no_sni
262296
# terminate ssl on edge
263297
bind 127.0.0.1:{{env "ROUTER_SERVICE_NO_SNI_PORT" "10443"}} ssl no-sslv3 crt {{firstMatch ".+" .DefaultCertificate "/var/lib/haproxy/conf/default_pub_keys.pem"}} accept-proxy
298+
{{- with (env "ROUTER_MUTUAL_TLS_AUTH_CA") }} ca-file {{.}} {{ end }}
299+
{{- with (env "ROUTER_MUTUAL_TLS_AUTH_CRL") }} crl-file {{.}} {{ end }}
300+
{{- with (env "ROUTER_MUTUAL_TLS_AUTH") }} verify {{.}} {{ end }}
264301
mode http
265302

266303
# Strip off Proxy headers to prevent HTTpoxy (https://httpoxy.org/)
@@ -270,6 +307,29 @@ frontend fe_no_sni
270307
# before matching, or any requests containing uppercase characters will never match.
271308
http-request set-header Host %[req.hdr(Host),lower]
272309

310+
{{ if ne (env "ROUTER_MUTUAL_TLS_AUTH" "none") "none" }}
311+
{{- with (env "ROUTER_MUTUAL_TLS_AUTH_FILTER") }}
312+
# If a mutual TLS auth subject filter environment variable is set, we deny
313+
# requests if the DN field in the client certificate doesn't match that value.
314+
# Please note that this match is a regular expression match.
315+
# See the config section 'frontend fe_sni' for examples.
316+
acl cert_cn_matches ssl_c_s_dn -m reg {{.}}
317+
http-request deny unless cert_cn_matches
318+
{{- end }}
319+
320+
# Add X-SSL* headers to pass client certificate information to the backend.
321+
http-request set-header X-SSL %[ssl_fc]
322+
http-request set-header X-SSL-Client-Verify %[ssl_c_verify]
323+
http-request set-header X-SSL-Client-Serial %{+Q}[ssl_c_serial,hex]
324+
http-request set-header X-SSL-Client-Version %{+Q}[ssl_c_version]
325+
http-request set-header X-SSL-Client-SHA1 %{+Q}[ssl_c_sha1,hex]
326+
http-request set-header X-SSL-Client-DN %{+Q}[ssl_c_s_dn]
327+
http-request set-header X-SSL-Client-CN %{+Q}[ssl_c_s_dn(cn)]
328+
http-request set-header X-SSL-Issuer %{+Q}[ssl_c_i_dn]
329+
http-request set-header X-SSL-Client-NotBefore %{+Q}[ssl_c_notbefore]
330+
http-request set-header X-SSL-Client-NotAfter %{+Q}[ssl_c_notafter]
331+
http-request set-header X-SSL-Client-DER %{+Q}[ssl_c_der,base64]
332+
{{- end }}
273333

274334
# map to backend
275335
# Search from most specific to general path (host case).

pkg/oc/admin/router/router.go

Lines changed: 109 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1919
"k8s.io/apimachinery/pkg/runtime"
2020
"k8s.io/apimachinery/pkg/util/intstr"
21+
"k8s.io/apimachinery/pkg/util/sets"
2122
"k8s.io/apimachinery/pkg/util/validation"
2223
"k8s.io/client-go/dynamic"
2324
"k8s.io/kubernetes/pkg/api/legacyscheme"
@@ -84,6 +85,11 @@ var (
8485
privkeyName = "router.pem"
8586
privkeyPath = secretsPath + "/" + privkeyName
8687

88+
defaultMutualTLSAuth = "none"
89+
clientCertConfigDir = "/etc/pki/tls/client-certs"
90+
clientCertConfigCA = "ca.pem"
91+
clientCertConfigCRL = "crl.pem"
92+
8793
defaultCertificatePath = path.Join(defaultCertificateDir, "tls.crt")
8894
)
8995

@@ -237,6 +243,23 @@ type RouterConfig struct {
237243
Threads int32
238244

239245
Local bool
246+
247+
// MutualTLSAuth controls access to the router using a mutually agreed
248+
// upon TLS authentication mechanism (example client certificates).
249+
// One of: required | optional | none - the default is none.
250+
MutualTLSAuth string
251+
252+
// MutualTLSAuthCA contains the CA certificates that will be used
253+
// to verify a client's certificate.
254+
MutualTLSAuthCA string
255+
256+
// MutualTLSAuthCRL contains the certificate revocation list used to
257+
// verify a client's certificate.
258+
MutualTLSAuthCRL string
259+
260+
// MutualTLSAuthFilter contains the value to filter requests based on
261+
// a client certificate subject field substring match.
262+
MutualTLSAuthFilter string
240263
}
241264

242265
const (
@@ -271,6 +294,8 @@ func NewCmdRouter(f kcmdutil.Factory, parentName, name string, out, errout io.Wr
271294
StatsPort: defaultStatsPort,
272295
HostNetwork: true,
273296
HostPorts: true,
297+
298+
MutualTLSAuth: defaultMutualTLSAuth,
274299
}
275300

276301
cmd := &cobra.Command{
@@ -325,15 +350,25 @@ func NewCmdRouter(f kcmdutil.Factory, parentName, name string, out, errout io.Wr
325350
cmd.Flags().BoolVar(&cfg.Local, "local", cfg.Local, "If true, do not contact the apiserver")
326351
cmd.Flags().Int32Var(&cfg.Threads, "threads", cfg.Threads, "Specifies the number of threads for the haproxy router.")
327352

353+
cmd.Flags().StringVar(&cfg.MutualTLSAuth, "mutual-tls-auth", cfg.MutualTLSAuth, "Controls access to the router using mutually agreed upon TLS configuration (example client certificates). You can choose one of 'required', 'optional', or 'none'. The default is none.")
354+
cmd.Flags().StringVar(&cfg.MutualTLSAuthCA, "mutual-tls-auth-ca", cfg.MutualTLSAuthCA, "Optional path to a file containing one or more CA certificates used for mutual TLS authentication. The CA certificate[s] are used by the router to verify a client's certificate.")
355+
cmd.Flags().StringVar(&cfg.MutualTLSAuthCRL, "mutual-tls-auth-crl", cfg.MutualTLSAuthCRL, "Optional path to a file containing the certificate revocation list used for mutual TLS authentication. The certificate revocation list is used by the router to verify a client's certificate.")
356+
cmd.Flags().StringVar(&cfg.MutualTLSAuthFilter, "mutual-tls-auth-filter", cfg.MutualTLSAuthFilter, "Optional regular expression to filter the client certificates. If the client certificate subject field does _not_ match this regular expression, requests will be rejected by the router.")
357+
328358
cfg.Action.BindForOutput(cmd.Flags())
329359
cmd.Flags().String("output-version", "", "The preferred API versions of the output objects")
330360

331361
return cmd
332362
}
333363

364+
// generateMutualTLSSecretName generates a mutual TLS auth secret name.
365+
func generateMutualTLSSecretName(prefix string) string {
366+
return fmt.Sprintf("%s-mutual-tls-auth", prefix)
367+
}
368+
334369
// generateSecretsConfig generates any Secret and Volume objects, such
335370
// as SSH private keys, that are necessary for the router container.
336-
func generateSecretsConfig(cfg *RouterConfig, namespace string, defaultCert []byte, certName string) ([]*kapi.Secret, []kapi.Volume, []kapi.VolumeMount, error) {
371+
func generateSecretsConfig(cfg *RouterConfig, namespace, certName string, defaultCert, mtlsAuthCA, mtlsAuthCRL []byte) ([]*kapi.Secret, []kapi.Volume, []kapi.VolumeMount, error) {
337372
var secrets []*kapi.Secret
338373
var volumes []kapi.Volume
339374
var mounts []kapi.VolumeMount
@@ -440,6 +475,42 @@ func generateSecretsConfig(cfg *RouterConfig, namespace string, defaultCert []by
440475
}
441476
mounts = append(mounts, mount)
442477

478+
mtlsSecretData := map[string][]byte{}
479+
if len(mtlsAuthCA) > 0 {
480+
mtlsSecretData[clientCertConfigCA] = mtlsAuthCA
481+
}
482+
if len(mtlsAuthCRL) > 0 {
483+
mtlsSecretData[clientCertConfigCRL] = mtlsAuthCRL
484+
}
485+
486+
if len(mtlsSecretData) > 0 {
487+
secretName := generateMutualTLSSecretName(cfg.Name)
488+
secret := &kapi.Secret{
489+
ObjectMeta: metav1.ObjectMeta{
490+
Name: secretName,
491+
},
492+
Data: mtlsSecretData,
493+
}
494+
secrets = append(secrets, secret)
495+
496+
volume := kapi.Volume{
497+
Name: "mutual-tls-config",
498+
VolumeSource: kapi.VolumeSource{
499+
Secret: &kapi.SecretVolumeSource{
500+
SecretName: secretName,
501+
},
502+
},
503+
}
504+
volumes = append(volumes, volume)
505+
506+
mount := kapi.VolumeMount{
507+
Name: volume.Name,
508+
ReadOnly: true,
509+
MountPath: clientCertConfigDir,
510+
}
511+
mounts = append(mounts, mount)
512+
}
513+
443514
return secrets, volumes, mounts, nil
444515
}
445516

@@ -608,6 +679,14 @@ func RunCmdRouter(f kcmdutil.Factory, cmd *cobra.Command, out, errout io.Writer,
608679
if err != nil {
609680
return fmt.Errorf("error getting client: %v", err)
610681
}
682+
683+
if len(cfg.MutualTLSAuthCA) > 0 || len(cfg.MutualTLSAuthCRL) > 0 {
684+
secretName := generateMutualTLSSecretName(cfg.Name)
685+
if _, err := kClient.Core().Secrets(namespace).Get(secretName, metav1.GetOptions{}); err == nil {
686+
return fmt.Errorf("router could not be created: mutual tls secret %q already exists", secretName)
687+
}
688+
}
689+
611690
service, err := kClient.Core().Services(namespace).Get(name, metav1.GetOptions{})
612691
if err != nil {
613692
if !generate {
@@ -660,6 +739,20 @@ func RunCmdRouter(f kcmdutil.Factory, cmd *cobra.Command, out, errout io.Writer,
660739
return fmt.Errorf("router could not be created; error reading default certificate file: %v", err)
661740
}
662741

742+
mtlsAuthOptions := []string{"required", "optional", "none"}
743+
allowedMutualTLSAuthOptions := sets.NewString(mtlsAuthOptions...)
744+
if !allowedMutualTLSAuthOptions.Has(cfg.MutualTLSAuth) {
745+
return fmt.Errorf("invalid mutual tls auth option %v, expected one of %v", cfg.MutualTLSAuth, mtlsAuthOptions)
746+
}
747+
mtlsAuthCA, err := fileutil.LoadData(cfg.MutualTLSAuthCA)
748+
if err != nil {
749+
return fmt.Errorf("reading ca certificates for mutual tls auth: %v", err)
750+
}
751+
mtlsAuthCRL, err := fileutil.LoadData(cfg.MutualTLSAuthCRL)
752+
if err != nil {
753+
return fmt.Errorf("reading certificate revocation list for mutual tls auth: %v", err)
754+
}
755+
663756
if len(cfg.StatsPassword) == 0 {
664757
cfg.StatsPassword = generateStatsPassword()
665758
if !cfg.Action.ShouldPrint() {
@@ -719,6 +812,20 @@ func RunCmdRouter(f kcmdutil.Factory, cmd *cobra.Command, out, errout io.Writer,
719812
env["ROUTER_METRICS_TLS_CERT_FILE"] = "/etc/pki/tls/metrics/tls.crt"
720813
env["ROUTER_METRICS_TLS_KEY_FILE"] = "/etc/pki/tls/metrics/tls.key"
721814
}
815+
mtlsAuth := strings.TrimSpace(cfg.MutualTLSAuth)
816+
if len(mtlsAuth) > 0 && mtlsAuth != defaultMutualTLSAuth {
817+
env["ROUTER_MUTUAL_TLS_AUTH"] = cfg.MutualTLSAuth
818+
if len(mtlsAuthCA) > 0 {
819+
env["ROUTER_MUTUAL_TLS_AUTH_CA"] = path.Join(clientCertConfigDir, clientCertConfigCA)
820+
}
821+
if len(mtlsAuthCRL) > 0 {
822+
env["ROUTER_MUTUAL_TLS_AUTH_CRL"] = path.Join(clientCertConfigDir, clientCertConfigCRL)
823+
}
824+
if len(cfg.MutualTLSAuthFilter) > 0 {
825+
env["ROUTER_MUTUAL_TLS_AUTH_FILTER"] = strings.Replace(cfg.MutualTLSAuthFilter, " ", "\\ ", -1)
826+
}
827+
}
828+
722829
env.Add(secretEnv)
723830
if len(defaultCert) > 0 {
724831
if cfg.SecretsAsEnv {
@@ -729,7 +836,7 @@ func RunCmdRouter(f kcmdutil.Factory, cmd *cobra.Command, out, errout io.Writer,
729836
}
730837
env.Add(app.Environment{"DEFAULT_CERTIFICATE_DIR": defaultCertificateDir})
731838
var certName = fmt.Sprintf("%s-certs", cfg.Name)
732-
secrets, volumes, routerMounts, err := generateSecretsConfig(cfg, namespace, defaultCert, certName)
839+
secrets, volumes, routerMounts, err := generateSecretsConfig(cfg, namespace, certName, defaultCert, mtlsAuthCA, mtlsAuthCRL)
733840
if err != nil {
734841
return fmt.Errorf("router could not be created: %v", err)
735842
}

0 commit comments

Comments
 (0)