Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Router support for mutual tls authentication #19891

Merged
merged 1 commit into from
Jul 12, 2018

Conversation

ramr
Copy link
Contributor

@ramr ramr commented May 31, 2018

Changes as per RFE: https://bugzilla.redhat.com/show_bug.cgi?id=1477349

Adds mutual tls authentication support. Verification is via client-side certificates and its
use can be mandated to be either required or optional.
Additionally, an env variable ROUTER_MUTUAL_TLS_AUTH_CN provides more fine-grain
control on access based on certificate common names.

Usage:
Start the router with the new command line options to specify the mutual tls options.
Example:

$ oc adm router --latest-images=true  --mutual-tls-auth=required  \
       --mutual-tls-auth-ca=_ramr/nodejs-header-echo/config/CA/cacert.pem  \
       --mutual-tls-auth-filter='^/CN=header.test/ST=CA/C=US/O=Security/OU=OpenShift3$'

In the above example, the regular expression for mutual-tls-auth-filter forces an exact match with a certificate's subject.

To Test:

curl -insecure --cert-type pem --cert  _ramr/nodejs-header-echo/config/cert.pem  \
       --key _ramr/nodejs-header-echo/config/key.pem  -k -vvv  \
       --resolve reencrypt.header.test:443:127.0.0.1 https://reencrypt.header.test/

And test without the certificate/key to check that access is not allowed.
Other test scenarios can use different combinations of the filter (and/or --mutual-tls-auth flag) to test access.

@knobunc PTAL Thx

Edited Updated usage and tests

@openshift-ci-robot openshift-ci-robot added the size/L Denotes a PR that changes 100-499 lines, ignoring generated files. label May 31, 2018
@mrogers950
Copy link
Contributor

cc @openshift/sig-security

Copy link
Contributor

@simo5 simo5 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nits

{{- if ne (env "ROUTER_MUTUAL_TLS_AUTH" "none") "none" }}
{{- with (env "ROUTER_MUTUAL_TLS_AUTH_CN") }}
# If a mutual TLS auth CN is set, we deny requests if the common name doesn't
# match. A custom template can change this behavior (e.g. set custom headers).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please specify exactly what name the cert needs to match

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Common Name ... will update this to make this clearer.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not asking what CN is, I am asking what it should match, it is not clear to me what you have to set CN to.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So to make it a wee bit clearer:

  1. If the DN set to /CN=header.test/ST=CA/C=US/O=Security/OU=OpenShift3.
  2. A couple (example set) of the headers set in the request sent to the backend would be:
    X-SSL-Client-DN=/CN=header.test/ST=CA/C=US/O=Security/OU=OpenShift3
    X-SSL-Client-CN=header.test

And the ROUTER_MUTUAL_TLS_AUTH_CN environment variable value allows you to "filter" requests based on that CN value (header.test).

Example:
A. For ROUTER_MUTUAL_TLS_AUTH_CN=der.test or ROUTER_MUTUAL_TLS_AUTH_CN=header.te would match the value and the request would be sent to the backend.
B. For ROUTER_MUTUAL_TLS_AUTH_CN=MyClient, the request would be rejected.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sounds wrong, we should probably have a ROUTER_MUTUAL_TLS_AUTH_DN variable and match the exact subject as a default.
In what case matching a substring of a random CN in the subject is useful ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's what the customer was using/asking - ref: https://bugzilla.redhat.com/show_bug.cgi?id=1477349

In any case, this is moot because I just changed this to be an auth filter (see latest commit) to filter based on the subject (substring) so you can run a router ala:

oc adm router --latest-images=true  --mutual-tls-auth=optional  \
              --mutual-tls-auth-ca=_ramr/nodejs-header-echo/config/CA/cacert.pem  \
              --mutual-tls-auth-filter="CN=header.test"

As re: an exact match, see the other comment re: filtering on organizations or OUs.

{{- if ne (env "ROUTER_MUTUAL_TLS_AUTH" "none") "none" }}
{{- with (env "ROUTER_MUTUAL_TLS_AUTH_CN") }}
# If a mutual TLS auth CN is set, we deny requests if the common name doesn't
# match. A custom template can change this behavior (e.g. set custom headers).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above

http-request set-header X-SSL-Issuer %{+Q}[ssl_c_i_dn]
http-request set-header X-SSL-Client-NotBefore %{+Q}[ssl_c_notbefore]
http-request set-header X-SSL-Client-NotAfter %{+Q}[ssl_c_notafter]
{{- end }}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice to have access to the whole client cert, is it possible via env vars ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok will do - there's a ssl_c_der that gives the client cert in DER format (will convert to base64 if possible [otherwise hex] as it is in binary form).


// MutualTLSAuthCRL contains the certificate revocation list used to
// verify a client's certificate.
MutualTLSAuthCRL string
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given we default to no mutual TLS, it may be nice to specify a field with the Subject the cert need to match so that we do not need custom templates but a simple configuration change.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, ok - let me check with @knobunc on today's call on that.
Reason is that I felt the ROUTER_MUTUAL_TLS_AUTH_CN might not even be needed.
For the specific customer use case, they need to add/remove custom headers, so they would need a custom template to filter on the CN and then do the custom header magic anyway.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, am not sure doing this based on the subject may be possible. I did just briefly glance at the haproxy docs (ref: https://cbonte.github.io/haproxy-dconv/1.8/configuration.html#7.3.4) and didn't see a way to get at the subject field easily.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The subject is in SSL_CLIENT_S_DN

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or ssl_c_s_dn in haproxy

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, will rework this a bit later this afternoon to pass that filtering value as a command line option. And to do a substring match on that field ssl_c_s_dn. Thx

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you probably want an exact match

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so the existing RFE from https://bugzilla.redhat.com/show_bug.cgi?id=1477349 which the customer has uses a substring match.
In general, an exact match will be too restrictive if for example you want to filter on just say organizations, departments/units in an organization etc.

// generateSecretsConfig generates any Secret and Volume objects, such
// as SSH private keys, that are necessary for the router container.
func generateSecretsConfig(cfg *RouterConfig, namespace string, defaultCert []byte, certName string) ([]*kapi.Secret, []kapi.Volume, []kapi.VolumeMount, error) {
func generateSecretsConfig(cfg *RouterConfig, namespace string, certName string, defaultCert, mtlsAuthCA, mtlsAuthCRL []byte) ([]*kapi.Secret, []kapi.Volume, []kapi.VolumeMount, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May as well also drop string after namespace if we are into reordering arguments

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doh!! Thanks for catching that.

mtlsAuth := strings.TrimSpace(cfg.MutualTLSAuth)
if len(mtlsAuth) > 0 && mtlsAuth != defaultMutualTLSAuth {
env["ROUTER_MUTUAL_TLS_AUTH"] = cfg.MutualTLSAuth
if len(mtlsAuthCA) > 0 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it use the system store if the CA flag is not set? Otherwise it should be required when mTLS is enabled

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The defaults section in the haproxy-config.template sets the default ca-base to the system store which gets used if one is not specified on the bind directive.

@@ -309,15 +334,25 @@ func NewCmdRouter(f *clientcmd.Factory, parentName, name string, out, errout io.
cmd.Flags().BoolVar(&cfg.StrictSNI, "strict-sni", cfg.StrictSNI, "Use strict-sni bind processing (do not use default cert). Not supported for F5.")
cmd.Flags().BoolVar(&cfg.Local, "local", cfg.Local, "If true, do not contact the apiserver")

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.")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(ala client certificates) ala a typo ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Meaning as an example but I will "rework" that text along with any other new review comments. Thx

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ala is fine, but slightly obscure -- https://en.wiktionary.org/wiki/ala#Etymology_2

Might be better to say i.e.

@knobunc
Copy link
Contributor

knobunc commented Jun 5, 2018

/hold
Until master opens for new features

@openshift-ci-robot openshift-ci-robot added the do-not-merge/hold Indicates that a PR should not merge because someone has issued a /hold command. label Jun 5, 2018
{{- with (env "ROUTER_MUTUAL_TLS_AUTH_FILTER") }}
# If a mutual TLS auth subject filter environment variable is set, we deny
# requests if the DN field in the client certificate doesn't match that value.
# Please note that this match is a subset (substring) match.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you give an example showing that "head" would also match? And if you can anchor it so that it is an exact match? Thanks

# If a mutual TLS auth subject filter environment variable is set, we deny
# requests if the DN field in the client certificate doesn't match that value.
# Please note that this match is a subset (substring) match.
# Example: For DN set to: /CN=header.test/ST=CA/C=US/O=Security/OU=OpenShift3,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's just lose these examples and refer them to the examples above?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done and referred to the examples above.

@@ -309,15 +334,25 @@ func NewCmdRouter(f *clientcmd.Factory, parentName, name string, out, errout io.
cmd.Flags().BoolVar(&cfg.StrictSNI, "strict-sni", cfg.StrictSNI, "Use strict-sni bind processing (do not use default cert). Not supported for F5.")
cmd.Flags().BoolVar(&cfg.Local, "local", cfg.Local, "If true, do not contact the apiserver")

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.")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ala is fine, but slightly obscure -- https://en.wiktionary.org/wiki/ala#Etymology_2

Might be better to say i.e.

@knobunc knobunc self-assigned this Jun 5, 2018
@knobunc
Copy link
Contributor

knobunc commented Jun 5, 2018

/retest

@acomabon
Copy link

Any news about this merge?

@enj
Copy link
Contributor

enj commented Jun 14, 2018

Master is frozen from anything but critical fixes until the release-3.10 branch is cut.

Copy link
Contributor

@knobunc knobunc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/lgtm

@openshift-ci-robot openshift-ci-robot added the lgtm Indicates that a PR is ready to be merged. label Jun 14, 2018
@knobunc
Copy link
Contributor

knobunc commented Jun 27, 2018

/hold cancel

@openshift-ci-robot openshift-ci-robot removed the do-not-merge/hold Indicates that a PR should not merge because someone has issued a /hold command. label Jun 27, 2018
Copy link

@pravisankar pravisankar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/lgtm

http-request set-header X-SSL-Client-NotAfter %{+Q}[ssl_c_notafter]
http-request set-header X-SSL-Client-DER %{+Q}[ssl_c_der,base64]
{{- end }}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these headers required for edge routes which use plain http backend ? seems just re-encrypt route need them.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No strong opinion either way. But the headers do reflect information about the connection as it was handled on the edge by the router, so it is still relevant request info (irrespective of the type of backend/route) much like the [X-]Forwarded[-*] and Via headers.

@knobunc
Copy link
Contributor

knobunc commented Jul 9, 2018

@juanvallejo @soltysh @enj -- Can someone approve the completion changes please.


// MutualTLSAuthFilter contains the value to filter requests based on
// a client certificate subject field substring match.
MutualTLSAuthFilter string
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still do not think this should be a substring match.
In what case would you want to match only part of a cert without explicitly specifying which part at least ?
Unless there is a very specific reason to use a substring match we should only do an exact match.
Otherwise we risk opening up users to easy attacks.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The intent here is to restrict an already validated set of requests. It is important to note here that the connections are already filtered based on the CA signing the certificate (and/or any revoked certificates).

So the ROUTER_MUTUAL_TLS_AUTH_CA and ROUTER_MUTUAL_TLS_AUTH_CRL control the first pass of which connections (or certificates) are allowed and the substring match here is the second pass that is restricting that first block of connections to a smaller subset.

If we restrict that to specific match, then we can really only allow one single certifying authority per router. Multiple orgs/depts/units cannot be serviced by a single router instance.

This allows us flexibility to support that and if you really wanted a further locked down environment, you could just as well make the substring match more restrictive ala:
ROUTER_MUTUAL_TLS_AUTH_FILTER="/CN=header.test/ST=CA/C=US/O=Security/OU=OpenShift3" which effectively becomes a specific match.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Won't this match also: /CN=blah/CN=blah/CN=more-blah/CN=header.test/ST=CA/C=US/O=Security/OU=OpenShift3 ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it would string substring match that. But that said, is that a valid DN/subject?
From the looks of it the / is a separator in the subject, correct?
So shouldn't that be escaped if someone forces that into an input field? Otherwise, there's potential of other issues, no?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@simo5 also pertinent here is the fact that the ROUTER_MUTUAL_TLS_AUTH_FILTER is optional. Meaning if its not specified, we don't do any restrictive checks. This is a secondary check.

But if this is still a sticking point for you, we could potentially change the substring match to a regular expression ... so you could potentially do a ^<escaped-text>$ kind of expression for an exact match.
That said, it does make the user interface a bit more awkward as it requires the user to specify a regular expression. Not a back breaker though!
And it is worth noting that a regular expression without the positional anchor ... ala CN=foo would still match a /CN=blah/CN=foo/CN=blah/ string.

@knobunc / @ironcladlou any issues with that?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can have as many CN elements as you want in a DN.
Changing it to a regular expression would be definitely better and I would accept as a solution because then you can do exact matches if you want or anything.

An example with exact match and one with subfiltering should be provided as doc then.

@juanvallejo
Copy link
Contributor

completions lgtm

@juanvallejo
Copy link
Contributor

/approve

for completions

@ramr
Copy link
Contributor Author

ramr commented Jul 10, 2018

@simo5 changed to use a regular expression. Commit sha: 06615ab

No changes to the usage/command line except that the string is now a regular expression ala:

$ oc adm router --latest-images=true  --mutual-tls-auth=required   \
     --mutual-tls-auth-ca=_ramr/nodejs-header-echo/config/CA/cacert.pem  \
     --mutual-tls-auth-filter='^/CN=header.test/ST=CA/C=US/O=Security/OU=OpenShift3$'

The mutual-tls-auth-filter is a regular expression representing an exact match for the certificate subject.

@ramr
Copy link
Contributor Author

ramr commented Jul 10, 2018

@juanvallejo completions didn't change but in case you want to re-verify - the last commit only changed the command line option help text. Thx

@ramr
Copy link
Contributor Author

ramr commented Jul 11, 2018

Rebased with upstream to fix conflict in images/router/haproxy/conf/haproxy-config.template.

@ramr
Copy link
Contributor Author

ramr commented Jul 11, 2018

/retest

@simo5
Copy link
Contributor

simo5 commented Jul 11, 2018

please squash commits and I'll apply lgtm

@ramr
Copy link
Contributor Author

ramr commented Jul 11, 2018

@simo5 squashed the commits.

@openshift-ci-robot openshift-ci-robot added the lgtm Indicates that a PR is ready to be merged. label Jul 11, 2018
Copy link
Contributor

@knobunc knobunc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/lgtm

@knobunc
Copy link
Contributor

knobunc commented Jul 11, 2018

@simo5 -- Over to you

@simo5
Copy link
Contributor

simo5 commented Jul 11, 2018

/lgtm

@simo5
Copy link
Contributor

simo5 commented Jul 11, 2018

/retest

@ramr
Copy link
Contributor Author

ramr commented Jul 11, 2018

Opened new issue - test flake #20295

@ramr
Copy link
Contributor Author

ramr commented Jul 11, 2018

/retest

…ient

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.
@openshift-ci-robot openshift-ci-robot removed the lgtm Indicates that a PR is ready to be merged. label Jul 12, 2018
@ramr
Copy link
Contributor Author

ramr commented Jul 12, 2018

@knobunc may need a new /lgtm as there was a conflict in router.go after the --extended-logging option PR merged. So I had to rebase (and fix the conflict and squash) this PR. Thx

@simo5
Copy link
Contributor

simo5 commented Jul 12, 2018

/lgtm

@openshift-ci-robot openshift-ci-robot added the lgtm Indicates that a PR is ready to be merged. label Jul 12, 2018
@simo5
Copy link
Contributor

simo5 commented Jul 12, 2018

/retest

@openshift-ci-robot
Copy link

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: juanvallejo, knobunc, pravisankar, ramr, simo5

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@knobunc
Copy link
Contributor

knobunc commented Jul 12, 2018

/retest

@openshift-ci-robot
Copy link

openshift-ci-robot commented Jul 12, 2018

@ramr: The following test failed, say /retest to rerun them all:

Test name Commit Details Rerun command
ci/openshift-jenkins/unit 770687a link /test unit

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes/test-infra repository. I understand the commands that are listed here.

@ramr
Copy link
Contributor Author

ramr commented Jul 12, 2018

/test unit
/test end_to_end

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
approved Indicates a PR has been approved by an approver from all required OWNERS files. lgtm Indicates that a PR is ready to be merged. sig/security size/L Denotes a PR that changes 100-499 lines, ignoring generated files.
Projects
None yet
Development

Successfully merging this pull request may close these issues.