Skip to content

Commit e789a67

Browse files
committed
Add mutual tls auth support to the router. The verification is via client
side certificates and its use can be mandated to be either required or optional. In addition, an env variable ROUTER_MUTUAL_TLS_AUTH_CN provides more fine-grain control on access (via regular expressions) based on certificate common names.
1 parent da49f70 commit e789a67

File tree

4 files changed

+185
-2
lines changed

4 files changed

+185
-2
lines changed

Diff for: contrib/completions/bash/oc

+8
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: contrib/completions/zsh/oc

+8
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: images/router/haproxy/conf/haproxy-config.template

+60
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).

Diff for: pkg/oc/admin/router/router.go

+109-2
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)