[server/identifier] Support SPIFFEID use in authz (#5742)

When using OPA TLS authorization, authz policy authors will now have
access to the client certificates presented as part of the TLS
connection. This new data will be available under the key `client_certificates`.

The existing functionality where `identity` is set to the Subject RDN Sequence
is left unchanged.

When using x.509 SVIDs the SPIFFEID is presented as in the SANs of the
certificate. So this change makes that data available should users
require it. It also makes other information about the client cert
available to authz policy too.

Signed-off-by: Charlie Egan <charlie@styra.com>
This commit is contained in:
Charlie Egan
2023-03-09 16:42:59 +00:00
committed by GitHub
parent f24f18e512
commit b36da40ab6
10 changed files with 197 additions and 72 deletions
+5
View File
@@ -198,6 +198,11 @@ func makeInput(r *http.Request) (*http.Request, interface{}, error) {
input["identity"] = identity
}
clientCertificates, ok := identifier.ClientCertificates(r)
if ok {
input["client_certificates"] = clientCertificates
}
return r, input, nil
}
+25
View File
@@ -0,0 +1,25 @@
package identifier
import (
"context"
"crypto/x509"
"net/http"
)
type clientCertificatesKey string
const clientCertificates = clientCertificatesKey("org.openpolicyagent/client-certificates")
// ClientCertificates returns the ClientCertificates of the caller associated with ctx.
func ClientCertificates(r *http.Request) ([]*x509.Certificate, bool) {
ctx := r.Context()
certs, ok := ctx.Value(clientCertificates).([]*x509.Certificate)
return certs, ok
}
// SetClientCertificates returns a new http.Request with the ClientCertificates set to v.
func SetClientCertificates(r *http.Request, v []*x509.Certificate) *http.Request {
return r.WithContext(context.WithValue(r.Context(), clientCertificates, v))
}
+5 -33
View File
@@ -2,15 +2,18 @@
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
// Package identifier provides handlers for associating an identity with incoming requests.
// Package identifier provides handlers for associating identity information with incoming requests.
package identifier
import (
"context"
"net/http"
"regexp"
)
type identityKey string
const identity = identityKey("org.openpolicyagent/identity")
// Identity returns the identity of the caller associated with ctx.
func Identity(r *http.Request) (string, bool) {
ctx := r.Context()
@@ -25,34 +28,3 @@ func Identity(r *http.Request) (string, bool) {
func SetIdentity(r *http.Request, v string) *http.Request {
return r.WithContext(context.WithValue(r.Context(), identity, v))
}
type identityKey string
const identity = identityKey("org.openpolicyagent/identity")
// TokenBased extracts Bearer tokens from the request.
type TokenBased struct {
inner http.Handler
}
// NewTokenBased returns a new TokenBased object.
func NewTokenBased(inner http.Handler) *TokenBased {
return &TokenBased{
inner: inner,
}
}
var bearerTokenRegexp = regexp.MustCompile(`^Bearer\s+(\S+)$`)
func (h *TokenBased) ServeHTTP(w http.ResponseWriter, r *http.Request) {
value := r.Header.Get("Authorization")
if len(value) > 0 {
match := bearerTokenRegexp.FindStringSubmatch(value)
if len(match) > 0 {
r = SetIdentity(r, match[1])
}
}
h.inner.ServeHTTP(w, r)
}
+21
View File
@@ -0,0 +1,21 @@
package identifier_test
import (
"crypto/x509"
"net/http"
"github.com/open-policy-agent/opa/server/identifier"
)
type mockHandler struct {
identity string
identityDefined bool
clientCertificates []*x509.Certificate
clientCertificatesDefined bool
}
func (h *mockHandler) ServeHTTP(_ http.ResponseWriter, r *http.Request) {
h.identity, h.identityDefined = identifier.Identity(r)
h.clientCertificates, h.clientCertificatesDefined = identifier.ClientCertificates(r)
}
+14
View File
@@ -0,0 +1,14 @@
-----BEGIN CERTIFICATE-----
MIICMjCCAdegAwIBAgIIKoc+GoX4gH8wCgYIKoZIzj0EAwIwXDELMAkGA1UEBhMC
R0IxFDASBgNVBAoTC0V4YW1wbGUgT3JnMRkwFwYDVQQLExBFeGFtcGxlIE9yZyBV
bml0MRwwGgYDVQQFExMxNTEwNjM0Mzk1MTQyOTY4MzA4MCAXDTIzMDIyMzE2NTYw
M1oYDzIxMjIwMTMwMTY1NjAzWjBcMQswCQYDVQQGEwJHQjEUMBIGA1UEChMLRXhh
bXBsZSBPcmcxGTAXBgNVBAsTEEV4YW1wbGUgT3JnIFVuaXQxHDAaBgNVBAUTEzMw
NjQ0ODYzNTUwODY2MzkyMzEwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATsy0fp
ZR6jixkkbWFEvjPhymHX5qo0w7FTfXEzYmmjF2neQv3IWRFCBwbyb5qn1sy0mlkv
MvZY/7KxqEBYf4YLo4GAMH4wDgYDVR0PAQH/BAQDAgeAMB0GA1UdJQQWMBQGCCsG
AQUFBwMCBggrBgEFBQcDATAMBgNVHRMBAf8EAjAAMD8GA1UdEQQ4MDaGNHNwaWZm
ZTovL2V4YW1wbGUuY29tL3Byb2QvZXUvY2x1c3Rlci9zZXJ2aWNlcy9mb29iYXIw
CgYIKoZIzj0EAwIDSQAwRgIhANmslXIVd++AgReE1oVUkCGQqSZLHs6F6Q4qS8Pb
/YAQAiEAy4FtOuqHyI2jKBZJjHXYYtE1mQPicAC9E1+fhLLRhg4=
-----END CERTIFICATE-----
+5
View File
@@ -0,0 +1,5 @@
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgC40SqDYRW5hWbr0P
ofGXXE355mXlYIEOSMWYCaE553OhRANCAATsy0fpZR6jixkkbWFEvjPhymHX5qo0
w7FTfXEzYmmjF2neQv3IWRFCBwbyb5qn1sy0mlkvMvZY/7KxqEBYf4YL
-----END PRIVATE KEY-----
+1
View File
@@ -24,6 +24,7 @@ func (h *TLSBased) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if tls := r.TLS; tls != nil {
if certs := tls.PeerCertificates; len(certs) > 0 {
r = SetIdentity(r, certs[0].Subject.ToRDNSequence().String())
r = SetClientCertificates(r, certs)
}
}
+68 -22
View File
@@ -6,6 +6,8 @@ package identifier_test
import (
"crypto/tls"
"crypto/x509"
"encoding/pem"
"net/http"
"net/http/httptest"
"testing"
@@ -22,33 +24,46 @@ func TestTLSBased(t *testing.T) {
handler := identifier.NewTLSBased(mock)
tests := []struct {
desc string
cert string
key string
expected string
defined bool
desc string
cert string
key string
identityExpected string
identityDefined bool
clientCertificatesDefined bool
}{
{
desc: "no cert",
},
{
desc: "cert with CN=<name>",
cert: "testdata/cn-cert.pem",
key: "testdata/key.pem",
expected: "CN=my-client",
defined: true,
desc: "cert with CN=<name>",
cert: "testdata/cn-cert.pem",
key: "testdata/key.pem",
identityExpected: "CN=my-client",
identityDefined: true,
clientCertificatesDefined: true,
},
{
desc: "cert with long DN",
cert: "testdata/ou-cert.pem",
key: "testdata/key.pem",
expected: "OU=opa-client-01,O=Torchwood",
defined: true,
desc: "cert with long DN",
cert: "testdata/ou-cert.pem",
key: "testdata/key.pem",
identityExpected: "OU=opa-client-01,O=Torchwood",
identityDefined: true,
clientCertificatesDefined: true,
},
{
desc: "SPIFFE cert",
cert: "testdata/spiffe-svid-cert.pem",
key: "testdata/spiffe-svid-key.pem",
identityExpected: "SERIALNUMBER=3064486355086639231,OU=Example Org Unit,O=Example Org,C=GB",
identityDefined: true,
clientCertificatesDefined: true,
},
}
for _, tc := range tests {
t.Run(tc.desc, func(t *testing.T) {
var err error
// Note: some re-use happens if this server is outside of the tests loop,
// causing weird overlaps. Let's keep setting up a fresh one in each
// iteration to be safe.
@@ -58,26 +73,57 @@ func TestTLSBased(t *testing.T) {
defer s.Close()
c := s.Client() // trusts the httptest server's TLS cert
var cert tls.Certificate
if tc.cert != "" && tc.key != "" {
cert, err := tls.LoadX509KeyPair(tc.cert, tc.key)
cert, err = tls.LoadX509KeyPair(tc.cert, tc.key)
if err != nil {
t.Fatalf("read test cert/key (%s/%s): %s", tc.cert, tc.key, err)
}
c.Transport.(*http.Transport).TLSClientConfig.Certificates = []tls.Certificate{cert}
}
_, err := c.Get(s.URL)
_, err = c.Get(s.URL)
if err != nil {
t.Fatalf("unexpected error in GET %s: %s", s.URL, err)
}
if mock.defined != tc.defined {
t.Fatalf("Expected defined to be %v but got: %v", tc.defined, mock.defined)
if mock.identityDefined != tc.identityDefined {
t.Fatalf("Expected identityDefined to be %v but got: %v", tc.identityDefined, mock.identityDefined)
}
if mock.identity != tc.expected {
t.Fatalf("Expected identity to be %s but got: %s", tc.expected, mock.identity)
if tc.identityDefined {
if mock.identity != tc.identityExpected {
t.Fatalf("Expected identity to be %s but got: %s", tc.identityExpected, mock.identity)
}
}
if mock.clientCertificatesDefined != tc.clientCertificatesDefined {
t.Fatalf("Expected clientCertificatesDefined to be %v but got: %v", tc.clientCertificatesDefined, mock.clientCertificatesDefined)
}
if tc.clientCertificatesDefined {
if len(mock.clientCertificates) != 1 {
t.Fatalf("Expected clientCertificates to have 1 cert but got: %d", len(mock.clientCertificates))
}
gotPemData := pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: mock.clientCertificates[0].Raw,
})
parsedWantedCert, err := x509.ParseCertificate(cert.Certificate[0])
if err != nil {
t.Fatalf("Error parsing expected cert: %s", err)
}
wantPemData := pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: parsedWantedCert.Raw,
})
if got, want := string(gotPemData), string(wantPemData); got != want {
t.Fatalf("Expected clientCertificates to be \n%s\n but got: \n%s\n", want, got)
}
}
})
}
}
+33
View File
@@ -0,0 +1,33 @@
package identifier
import (
"net/http"
"regexp"
)
// TokenBased extracts Bearer tokens from the request.
type TokenBased struct {
inner http.Handler
}
// NewTokenBased returns a new TokenBased object.
func NewTokenBased(inner http.Handler) *TokenBased {
return &TokenBased{
inner: inner,
}
}
var bearerTokenRegexp = regexp.MustCompile(`^Bearer\s+(\S+)$`)
func (h *TokenBased) ServeHTTP(w http.ResponseWriter, r *http.Request) {
value := r.Header.Get("Authorization")
if len(value) > 0 {
match := bearerTokenRegexp.FindStringSubmatch(value)
if len(match) > 0 {
r = SetIdentity(r, match[1])
}
}
h.inner.ServeHTTP(w, r)
}
@@ -11,15 +11,6 @@ import (
"github.com/open-policy-agent/opa/server/identifier"
)
type mockHandler struct {
identity string
defined bool
}
func (h *mockHandler) ServeHTTP(_ http.ResponseWriter, r *http.Request) {
h.identity, h.defined = identifier.Identity(r)
}
func TestTokenBased(t *testing.T) {
mock := &mockHandler{}
@@ -31,13 +22,25 @@ func TestTokenBased(t *testing.T) {
}
tests := []struct {
value string
expected string
defined bool
value string
expected string
identityDefined bool
}{
{"", "", false},
{"Bearer this-is-the-token", "this-is-the-token", true},
{"Bearer this-is-the-token-with-spaces", "this-is-the-token-with-spaces", true},
{
"",
"",
false,
},
{
"Bearer this-is-the-token",
"this-is-the-token",
true,
},
{
"Bearer this-is-the-token-with-spaces",
"this-is-the-token-with-spaces",
true,
},
}
for _, tc := range tests {
@@ -48,8 +51,8 @@ func TestTokenBased(t *testing.T) {
handler.ServeHTTP(nil, req)
if mock.defined != tc.defined {
t.Fatalf("Expected defined to be %v but got: %v", tc.defined, mock.defined)
if mock.identityDefined != tc.identityDefined {
t.Fatalf("Expected identityDefined to be %v but got: %v", tc.identityDefined, mock.identityDefined)
}
if mock.identity != tc.expected {