authorize: incorporate mTLS validation from Envoy (#4374)

Configure Envoy to validate client certificates, using the union of all
relevant client CA bundles (that is, a bundle of the main client CA
setting together with all per-route client CAs). Pass the validation
status from Envoy through to the authorize service, by configuring Envoy
to use the newly-added SetClientCertificateMetadata filter, and by also
adding the relevant metadata namespace to the ExtAuthz configuration.

Remove the existing 'include_peer_certificate' setting from the ExtAuthz
configuration, as the metadata from the Lua filter will include the full
certificate chain (when it validates successfully by Envoy).

Update policy evaluation to consider the validation status from Envoy,
in addition to its own certificate chain validation. (Policy evaluation
cannot rely solely on the Envoy validation status while we still support
the per-route client CA setting.)
This commit is contained in:
Kenneth Jenkins 2023-07-21 12:17:01 -07:00 committed by GitHub
parent d2e8ecc7da
commit 8d09567fd7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 410 additions and 85 deletions

View file

@ -32,13 +32,13 @@ type Request struct {
// RequestHTTP is the HTTP field in the request.
type RequestHTTP struct {
Method string `json:"method"`
Hostname string `json:"hostname"`
Path string `json:"path"`
URL string `json:"url"`
Headers map[string]string `json:"headers"`
ClientCertificate string `json:"client_certificate"`
IP string `json:"ip"`
Method string `json:"method"`
Hostname string `json:"hostname"`
Path string `json:"path"`
URL string `json:"url"`
Headers map[string]string `json:"headers"`
ClientCertificate ClientCertificateInfo `json:"client_certificate"`
IP string `json:"ip"`
}
// NewRequestHTTP creates a new RequestHTTP.
@ -46,7 +46,7 @@ func NewRequestHTTP(
method string,
requestURL url.URL,
headers map[string]string,
rawClientCertificate string,
clientCertificate ClientCertificateInfo,
ip string,
) RequestHTTP {
return RequestHTTP{
@ -55,11 +55,33 @@ func NewRequestHTTP(
Path: requestURL.Path,
URL: requestURL.String(),
Headers: headers,
ClientCertificate: rawClientCertificate,
ClientCertificate: clientCertificate,
IP: ip,
}
}
// ClientCertificateInfo contains information about the certificate presented
// by the client (if any).
type ClientCertificateInfo struct {
// Presented is true if the client presented any certificate at all.
Presented bool `json:"presented"`
// Validated is true if the client presented a valid certificate with a
// trust chain rooted at any of the CAs configured within the Envoy
// listener. If any routes define a tls_downstream_client_ca, additional
// validation is required (for all routes).
Validated bool `json:"validated"`
// Leaf contains the leaf client certificate, provided that the certificate
// validated successfully.
Leaf string `json:"leaf,omitempty"`
// Intermediates contains the remainder of the client certificate chain as
// it was originally presented by the client, provided that the client
// certificate validated successfully.
Intermediates string `json:"intermediates,omitempty"`
}
// RequestSession is the session field in the request.
type RequestSession struct {
ID string `json:"id"`

View file

@ -116,6 +116,12 @@ func TestEvaluator(t *testing.T) {
WithPolicies(policies),
}
validCertInfo := ClientCertificateInfo{
Presented: true,
Validated: true,
Leaf: testValidCert,
}
t.Run("client certificate", func(t *testing.T) {
t.Run("invalid", func(t *testing.T) {
res, err := eval(t, options, nil, &Request{
@ -128,7 +134,7 @@ func TestEvaluator(t *testing.T) {
res, err := eval(t, options, nil, &Request{
Policy: &policies[0],
HTTP: RequestHTTP{
ClientCertificate: testValidCert,
ClientCertificate: validCertInfo,
},
})
require.NoError(t, err)
@ -154,7 +160,7 @@ func TestEvaluator(t *testing.T) {
HTTP: RequestHTTP{
Method: http.MethodGet,
URL: "https://from.example.com",
ClientCertificate: testValidCert,
ClientCertificate: validCertInfo,
},
})
require.NoError(t, err)
@ -179,7 +185,7 @@ func TestEvaluator(t *testing.T) {
HTTP: RequestHTTP{
Method: http.MethodGet,
URL: "https://from.example.com",
ClientCertificate: testValidCert,
ClientCertificate: validCertInfo,
},
})
require.NoError(t, err)
@ -206,7 +212,7 @@ func TestEvaluator(t *testing.T) {
HTTP: RequestHTTP{
Method: http.MethodGet,
URL: "https://from.example.com",
ClientCertificate: testValidCert,
ClientCertificate: validCertInfo,
},
})
require.NoError(t, err)
@ -230,7 +236,7 @@ func TestEvaluator(t *testing.T) {
HTTP: RequestHTTP{
Method: http.MethodGet,
URL: "https://from.example.com",
ClientCertificate: testValidCert,
ClientCertificate: validCertInfo,
},
})
require.NoError(t, err)
@ -254,7 +260,7 @@ func TestEvaluator(t *testing.T) {
HTTP: RequestHTTP{
Method: http.MethodGet,
URL: "https://from.example.com",
ClientCertificate: testValidCert,
ClientCertificate: validCertInfo,
},
})
require.NoError(t, err)
@ -285,7 +291,7 @@ func TestEvaluator(t *testing.T) {
HTTP: RequestHTTP{
Method: http.MethodGet,
URL: "https://from.example.com",
ClientCertificate: testValidCert,
ClientCertificate: validCertInfo,
},
})
require.NoError(t, err)
@ -310,7 +316,7 @@ func TestEvaluator(t *testing.T) {
HTTP: RequestHTTP{
Method: http.MethodGet,
URL: "https://from.example.com",
ClientCertificate: testValidCert,
ClientCertificate: validCertInfo,
},
})
require.NoError(t, err)
@ -334,7 +340,7 @@ func TestEvaluator(t *testing.T) {
HTTP: RequestHTTP{
Method: http.MethodGet,
URL: "https://from.example.com",
ClientCertificate: testValidCert,
ClientCertificate: validCertInfo,
},
})
require.NoError(t, err)
@ -363,7 +369,7 @@ func TestEvaluator(t *testing.T) {
HTTP: RequestHTTP{
Method: http.MethodGet,
URL: "https://from.example.com",
ClientCertificate: testValidCert,
ClientCertificate: validCertInfo,
},
})
require.NoError(t, err)
@ -386,7 +392,7 @@ func TestEvaluator(t *testing.T) {
HTTP: RequestHTTP{
Method: http.MethodGet,
URL: "https://from.example.com",
ClientCertificate: testValidCert,
ClientCertificate: validCertInfo,
},
})
require.NoError(t, err)
@ -423,7 +429,7 @@ func TestEvaluator(t *testing.T) {
HTTP: RequestHTTP{
Method: http.MethodGet,
URL: "https://from.example.com",
ClientCertificate: testValidCert,
ClientCertificate: validCertInfo,
Headers: tc.src,
},
})
@ -439,7 +445,7 @@ func TestEvaluator(t *testing.T) {
http.MethodGet,
*mustParseURL("https://from.example.com/"),
nil,
testValidCert,
validCertInfo,
"",
),
})
@ -453,7 +459,7 @@ func TestEvaluator(t *testing.T) {
"POST",
*mustParseURL("https://from.example.com/test"),
nil,
testValidCert,
validCertInfo,
"",
),
})

View file

@ -13,14 +13,15 @@ import (
var isValidClientCertificateCache, _ = lru.New2Q[[2]string, bool](100)
func isValidClientCertificate(ca, cert string) (bool, error) {
// when ca is the empty string, client certificates are always accepted
func isValidClientCertificate(ca string, certInfo ClientCertificateInfo) (bool, error) {
// when ca is the empty string, client certificates are not required
if ca == "" {
return true, nil
}
// when cert is the empty string, no client certificate was supplied
if cert == "" {
cert := certInfo.Leaf
if !certInfo.Validated || cert == "" {
return false, nil
}

View file

@ -95,27 +95,48 @@ Y+E5W+FKfIBv9yvdNBYZsL6IZ0Yh1ctKwB5gnajO8+swx5BeaCIbBrCtOBSB
func Test_isValidClientCertificate(t *testing.T) {
t.Run("no ca", func(t *testing.T) {
valid, err := isValidClientCertificate("", "WHATEVER!")
valid, err := isValidClientCertificate("", ClientCertificateInfo{Leaf: "WHATEVER!"})
assert.NoError(t, err, "should not return an error")
assert.True(t, valid, "should return true")
})
t.Run("no cert", func(t *testing.T) {
valid, err := isValidClientCertificate(testCA, "")
valid, err := isValidClientCertificate(testCA, ClientCertificateInfo{})
assert.NoError(t, err, "should not return an error")
assert.False(t, valid, "should return false")
})
t.Run("valid cert", func(t *testing.T) {
valid, err := isValidClientCertificate(testCA, testValidCert)
valid, err := isValidClientCertificate(testCA, ClientCertificateInfo{
Presented: true,
Validated: true,
Leaf: testValidCert,
})
assert.NoError(t, err, "should not return an error")
assert.True(t, valid, "should return true")
})
t.Run("cert not externally validated", func(t *testing.T) {
valid, err := isValidClientCertificate(testCA, ClientCertificateInfo{
Presented: true,
Validated: false,
Leaf: testValidCert,
})
assert.NoError(t, err, "should not return an error")
assert.False(t, valid, "should return false")
})
t.Run("unsigned cert", func(t *testing.T) {
valid, err := isValidClientCertificate(testCA, testUnsignedCert)
valid, err := isValidClientCertificate(testCA, ClientCertificateInfo{
Presented: true,
Validated: true,
Leaf: testUnsignedCert,
})
assert.NoError(t, err, "should not return an error")
assert.False(t, valid, "should return false")
})
t.Run("not a cert", func(t *testing.T) {
valid, err := isValidClientCertificate(testCA, "WHATEVER!")
valid, err := isValidClientCertificate(testCA, ClientCertificateInfo{
Presented: true,
Validated: true,
Leaf: "WHATEVER!",
})
assert.Error(t, err, "should return an error")
assert.False(t, valid, "should return false")
})