Skip to content

Commit d3894ad

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 based on certificate common names.
1 parent 6d89588 commit d3894ad

File tree

4 files changed

+160
-2
lines changed

4 files changed

+160
-2
lines changed

contrib/completions/bash/oc

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

contrib/completions/zsh/oc

+6
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

+47
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,9 @@ frontend fe_sni
219219
{{- if isTrue (env "ROUTER_STRICT_SNI") }} strict-sni {{ end }}
220220
{{- ""}} crt {{firstMatch ".+" .DefaultCertificate "/var/lib/haproxy/conf/default_pub_keys.pem"}}
221221
{{- ""}} crt-list /var/lib/haproxy/conf/cert_config.map accept-proxy
222+
{{- with (env "ROUTER_MUTUAL_TLS_AUTH_CA") }} ca-file {{.}} {{ end }}
223+
{{- with (env "ROUTER_MUTUAL_TLS_AUTH_CRL") }} crl-file {{.}} {{ end }}
224+
{{- with (env "ROUTER_MUTUAL_TLS_AUTH") }} verify {{.}} {{ end }}
222225
mode http
223226

224227
# Strip off Proxy headers to prevent HTTpoxy (https://httpoxy.org/)
@@ -228,6 +231,27 @@ frontend fe_sni
228231
# before matching, or any requests containing uppercase characters will never match.
229232
http-request set-header Host %[req.hdr(Host),lower]
230233

234+
{{- if ne (env "ROUTER_MUTUAL_TLS_AUTH" "none") "none" }}
235+
{{- with (env "ROUTER_MUTUAL_TLS_AUTH_CN") }}
236+
# If a mutual TLS auth CN is set, we deny requests if the common name doesn't
237+
# match. A custom template can change this behavior (e.g. set custom headers).
238+
acl cert_cn_matches ssl_c_s_dn(CN) -m sub {{.}}
239+
http-request deny unless cert_cn_matches
240+
{{- end }}
241+
242+
# Add X-SSL* headers to pass client certificate information to the backend.
243+
http-request set-header X-SSL %[ssl_fc]
244+
http-request set-header X-SSL-Client-Verify %[ssl_c_verify]
245+
http-request set-header X-SSL-Client-Serial %{+Q}[ssl_c_serial,hex]
246+
http-request set-header X-SSL-Client-Version %{+Q}[ssl_c_version]
247+
http-request set-header X-SSL-Client-SHA1 %{+Q}[ssl_c_sha1,hex]
248+
http-request set-header X-SSL-Client-DN %{+Q}[ssl_c_s_dn]
249+
http-request set-header X-SSL-Client-CN %{+Q}[ssl_c_s_dn(cn)]
250+
http-request set-header X-SSL-Issuer %{+Q}[ssl_c_i_dn]
251+
http-request set-header X-SSL-Client-NotBefore %{+Q}[ssl_c_notbefore]
252+
http-request set-header X-SSL-Client-NotAfter %{+Q}[ssl_c_notafter]
253+
{{- end }}
254+
231255
# map to backend
232256
# Search from most specific to general path (host case).
233257
# Note: If no match, haproxy uses the default_backend, no other
@@ -254,6 +278,9 @@ backend be_no_sni
254278
frontend fe_no_sni
255279
# terminate ssl on edge
256280
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
281+
{{- with (env "ROUTER_MUTUAL_TLS_AUTH_CA") }} ca-file {{.}} {{ end }}
282+
{{- with (env "ROUTER_MUTUAL_TLS_AUTH_CRL") }} crl-file {{.}} {{ end }}
283+
{{- with (env "ROUTER_MUTUAL_TLS_AUTH") }} verify {{.}} {{ end }}
257284
mode http
258285

259286
# Strip off Proxy headers to prevent HTTpoxy (https://httpoxy.org/)
@@ -263,6 +290,26 @@ frontend fe_no_sni
263290
# before matching, or any requests containing uppercase characters will never match.
264291
http-request set-header Host %[req.hdr(Host),lower]
265292

293+
{{- if ne (env "ROUTER_MUTUAL_TLS_AUTH" "none") "none" }}
294+
{{- with (env "ROUTER_MUTUAL_TLS_AUTH_CN") }}
295+
# If a mutual TLS auth CN is set, we deny requests if the common name doesn't
296+
# match. A custom template can change this behavior (e.g. set custom headers).
297+
acl cert_cn_matches ssl_c_s_dn(CN) -m sub {{.}}
298+
http-request deny unless cert_cn_matches
299+
{{- end }}
300+
301+
# Add X-SSL* headers to pass client certificate information to the backend.
302+
http-request set-header X-SSL %[ssl_fc]
303+
http-request set-header X-SSL-Client-Verify %[ssl_c_verify]
304+
http-request set-header X-SSL-Client-Serial %{+Q}[ssl_c_serial,hex]
305+
http-request set-header X-SSL-Client-Version %{+Q}[ssl_c_version]
306+
http-request set-header X-SSL-Client-SHA1 %{+Q}[ssl_c_sha1,hex]
307+
http-request set-header X-SSL-Client-DN %{+Q}[ssl_c_s_dn]
308+
http-request set-header X-SSL-Client-CN %{+Q}[ssl_c_s_dn(cn)]
309+
http-request set-header X-SSL-Issuer %{+Q}[ssl_c_i_dn]
310+
http-request set-header X-SSL-Client-NotBefore %{+Q}[ssl_c_notbefore]
311+
http-request set-header X-SSL-Client-NotAfter %{+Q}[ssl_c_notafter]
312+
{{- end }}
266313

267314
# map to backend
268315
# Search from most specific to general path (host case).

pkg/oc/admin/router/router.go

+101-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/kubernetes/pkg/api/legacyscheme"
2324
kapi "k8s.io/kubernetes/pkg/apis/core"
@@ -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

@@ -229,6 +235,19 @@ type RouterConfig struct {
229235
StrictSNI bool
230236

231237
Local bool
238+
239+
// MutualTLSAuth controls access to the router using a mutually agreed
240+
// upon TLS authentication mechanism (ala client certificates).
241+
// One of: required | optional | none - the default is none.
242+
MutualTLSAuth string
243+
244+
// MutualTLSAuthCA contains the CA certificates that will be used
245+
// to verify a client's certificate.
246+
MutualTLSAuthCA string
247+
248+
// MutualTLSAuthCRL contains the certificate revocation list used to
249+
// verify a client's certificate.
250+
MutualTLSAuthCRL string
232251
}
233252

234253
const (
@@ -257,6 +276,8 @@ func NewCmdRouter(f *clientcmd.Factory, parentName, name string, out, errout io.
257276
StatsPort: defaultStatsPort,
258277
HostNetwork: true,
259278
HostPorts: true,
279+
280+
MutualTLSAuth: defaultMutualTLSAuth,
260281
}
261282

262283
cmd := &cobra.Command{
@@ -309,15 +330,24 @@ func NewCmdRouter(f *clientcmd.Factory, parentName, name string, out, errout io.
309330
cmd.Flags().BoolVar(&cfg.StrictSNI, "strict-sni", cfg.StrictSNI, "Use strict-sni bind processing (do not use default cert). Not supported for F5.")
310331
cmd.Flags().BoolVar(&cfg.Local, "local", cfg.Local, "If true, do not contact the apiserver")
311332

333+
cmd.Flags().StringVar(&cfg.MutualTLSAuth, "mutual-tls-auth", cfg.MutualTLSAuth, "Controls access to the router using mutually agreed upon TLS configuration (ala client certificates). You can choose one of 'required', 'optional', or 'none'. The default is none.")
334+
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.")
335+
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.")
336+
312337
cfg.Action.BindForOutput(cmd.Flags())
313338
cmd.Flags().String("output-version", "", "The preferred API versions of the output objects")
314339

315340
return cmd
316341
}
317342

343+
// generateMutualTLSSecretName generates a mutual TLS auth secret name.
344+
func generateMutualTLSSecretName(prefix string) string {
345+
return fmt.Sprintf("%s-mutual-tls-auth", prefix)
346+
}
347+
318348
// generateSecretsConfig generates any Secret and Volume objects, such
319349
// as SSH private keys, that are necessary for the router container.
320-
func generateSecretsConfig(cfg *RouterConfig, namespace string, defaultCert []byte, certName string) ([]*kapi.Secret, []kapi.Volume, []kapi.VolumeMount, error) {
350+
func generateSecretsConfig(cfg *RouterConfig, namespace string, certName string, defaultCert, mtlsAuthCA, mtlsAuthCRL []byte) ([]*kapi.Secret, []kapi.Volume, []kapi.VolumeMount, error) {
321351
var secrets []*kapi.Secret
322352
var volumes []kapi.Volume
323353
var mounts []kapi.VolumeMount
@@ -424,6 +454,42 @@ func generateSecretsConfig(cfg *RouterConfig, namespace string, defaultCert []by
424454
}
425455
mounts = append(mounts, mount)
426456

457+
mtlsSecretData := map[string][]byte{}
458+
if len(mtlsAuthCA) > 0 {
459+
mtlsSecretData[clientCertConfigCA] = mtlsAuthCA
460+
}
461+
if len(mtlsAuthCRL) > 0 {
462+
mtlsSecretData[clientCertConfigCRL] = mtlsAuthCRL
463+
}
464+
465+
if len(mtlsSecretData) > 0 {
466+
secretName := generateMutualTLSSecretName(cfg.Name)
467+
secret := &kapi.Secret{
468+
ObjectMeta: metav1.ObjectMeta{
469+
Name: secretName,
470+
},
471+
Data: mtlsSecretData,
472+
}
473+
secrets = append(secrets, secret)
474+
475+
volume := kapi.Volume{
476+
Name: "mutual-tls-config",
477+
VolumeSource: kapi.VolumeSource{
478+
Secret: &kapi.SecretVolumeSource{
479+
SecretName: secretName,
480+
},
481+
},
482+
}
483+
volumes = append(volumes, volume)
484+
485+
mount := kapi.VolumeMount{
486+
Name: volume.Name,
487+
ReadOnly: true,
488+
MountPath: clientCertConfigDir,
489+
}
490+
mounts = append(mounts, mount)
491+
}
492+
427493
return secrets, volumes, mounts, nil
428494
}
429495

@@ -576,6 +642,14 @@ func RunCmdRouter(f *clientcmd.Factory, cmd *cobra.Command, out, errout io.Write
576642
if err != nil {
577643
return fmt.Errorf("error getting client: %v", err)
578644
}
645+
646+
if len(cfg.MutualTLSAuthCA) > 0 || len(cfg.MutualTLSAuthCRL) > 0 {
647+
secretName := generateMutualTLSSecretName(cfg.Name)
648+
if _, err := kClient.Core().Secrets(namespace).Get(secretName, metav1.GetOptions{}); err == nil {
649+
return fmt.Errorf("router could not be created: mutual tls secret %q already exists", secretName)
650+
}
651+
}
652+
579653
service, err := kClient.Core().Services(namespace).Get(name, metav1.GetOptions{})
580654
if err != nil {
581655
if !generate {
@@ -628,6 +702,20 @@ func RunCmdRouter(f *clientcmd.Factory, cmd *cobra.Command, out, errout io.Write
628702
return fmt.Errorf("router could not be created; error reading default certificate file: %v", err)
629703
}
630704

705+
mtlsAuthOptions := []string{"required", "optional", "none"}
706+
allowedMutualTLSAuthOptions := sets.NewString(mtlsAuthOptions...)
707+
if !allowedMutualTLSAuthOptions.Has(cfg.MutualTLSAuth) {
708+
return fmt.Errorf("invalid mutual tls auth option %v, expected one of %v", cfg.MutualTLSAuth, mtlsAuthOptions)
709+
}
710+
mtlsAuthCA, err := fileutil.LoadData(cfg.MutualTLSAuthCA)
711+
if err != nil {
712+
return fmt.Errorf("reading ca certificates for mutual tls auth: %v", err)
713+
}
714+
mtlsAuthCRL, err := fileutil.LoadData(cfg.MutualTLSAuthCRL)
715+
if err != nil {
716+
return fmt.Errorf("reading certificate revocation list for mutual tls auth: %v", err)
717+
}
718+
631719
if len(cfg.StatsPassword) == 0 {
632720
cfg.StatsPassword = generateStatsPassword()
633721
if !cfg.Action.ShouldPrint() {
@@ -685,6 +773,17 @@ func RunCmdRouter(f *clientcmd.Factory, cmd *cobra.Command, out, errout io.Write
685773
env["ROUTER_METRICS_TLS_CERT_FILE"] = "/etc/pki/tls/metrics/tls.crt"
686774
env["ROUTER_METRICS_TLS_KEY_FILE"] = "/etc/pki/tls/metrics/tls.key"
687775
}
776+
mtlsAuth := strings.TrimSpace(cfg.MutualTLSAuth)
777+
if len(mtlsAuth) > 0 && mtlsAuth != defaultMutualTLSAuth {
778+
env["ROUTER_MUTUAL_TLS_AUTH"] = cfg.MutualTLSAuth
779+
if len(mtlsAuthCA) > 0 {
780+
env["ROUTER_MUTUAL_TLS_AUTH_CA"] = path.Join(clientCertConfigDir, clientCertConfigCA)
781+
}
782+
if len(mtlsAuthCRL) > 0 {
783+
env["ROUTER_MUTUAL_TLS_AUTH_CRL"] = path.Join(clientCertConfigDir, clientCertConfigCRL)
784+
}
785+
}
786+
688787
env.Add(secretEnv)
689788
if len(defaultCert) > 0 {
690789
if cfg.SecretsAsEnv {
@@ -695,7 +794,7 @@ func RunCmdRouter(f *clientcmd.Factory, cmd *cobra.Command, out, errout io.Write
695794
}
696795
env.Add(app.Environment{"DEFAULT_CERTIFICATE_DIR": defaultCertificateDir})
697796
var certName = fmt.Sprintf("%s-certs", cfg.Name)
698-
secrets, volumes, mounts, err := generateSecretsConfig(cfg, namespace, defaultCert, certName)
797+
secrets, volumes, mounts, err := generateSecretsConfig(cfg, namespace, certName, defaultCert, mtlsAuthCA, mtlsAuthCRL)
699798
if err != nil {
700799
return fmt.Errorf("router could not be created: %v", err)
701800
}

0 commit comments

Comments
 (0)