Admin clientauthorizer implementation

This commit is contained in:
Mikalai Radchuk 2020-02-21 13:52:27 +00:00
Родитель e1692ab0ad
Коммит 35632b5048
7 изменённых файлов: 357 добавлений и 35 удалений

Просмотреть файл

@ -40,12 +40,29 @@ func rp(ctx context.Context, log *logrus.Entry) error {
uuid := uuid.NewV4().String()
log.Printf("uuid %s", uuid)
env, err := env.NewEnv(ctx, log)
_env, err := env.NewEnv(ctx, log)
if err != nil {
return err
}
m, err := statsd.New(ctx, log.WithField("component", "metrics"), env, os.Getenv("MDM_ACCOUNT"), os.Getenv("MDM_NAMESPACE"))
if _, ok := _env.(env.Dev); !ok {
for _, key := range []string{
"MDM_ACCOUNT",
"MDM_NAMESPACE",
"ADMIN_API_CLIENT_CERT_COMMON_NAME",
} {
if _, found := os.LookupEnv(key); !found {
return fmt.Errorf("environment variable %q unset", key)
}
}
}
err = _env.InitializeAuthorizers()
if err != nil {
return err
}
m, err := statsd.New(ctx, log.WithField("component", "metrics"), _env, os.Getenv("MDM_ACCOUNT"), os.Getenv("MDM_NAMESPACE"))
if err != nil {
return err
}
@ -53,22 +70,22 @@ func rp(ctx context.Context, log *logrus.Entry) error {
tracing.Register(azure.New(m))
metrics.Register(k8s.NewLatency(m), k8s.NewResult(m))
cipher, err := encryption.NewXChaCha20Poly1305(ctx, env)
cipher, err := encryption.NewXChaCha20Poly1305(ctx, _env)
if err != nil {
return err
}
db, err := database.NewDatabase(ctx, log.WithField("component", "database"), env, m, cipher, uuid)
db, err := database.NewDatabase(ctx, log.WithField("component", "database"), _env, m, cipher, uuid)
if err != nil {
return err
}
f, err := frontend.NewFrontend(ctx, log.WithField("component", "frontend"), env, db, api.APIs, m)
f, err := frontend.NewFrontend(ctx, log.WithField("component", "frontend"), _env, db, api.APIs, m)
if err != nil {
return err
}
b, err := backend.NewBackend(ctx, log.WithField("component", "backend"), env, db, m)
b, err := backend.NewBackend(ctx, log.WithField("component", "backend"), _env, db, m)
if err != nil {
return err
}

10
pkg/env/dev.go поставляемый
Просмотреть файл

@ -70,7 +70,7 @@ type dev struct {
proxyClientKey *rsa.PrivateKey
}
func newDev(ctx context.Context, log *logrus.Entry, instancemetadata instancemetadata.InstanceMetadata, armClientAuthorizer, adminClientAuthorizer clientauthorizer.ClientAuthorizer) (*dev, error) {
func newDev(ctx context.Context, log *logrus.Entry, instancemetadata instancemetadata.InstanceMetadata) (*dev, error) {
for _, key := range []string{
"AZURE_ARM_CLIENT_ID",
"AZURE_ARM_CLIENT_SECRET",
@ -97,7 +97,7 @@ func newDev(ctx context.Context, log *logrus.Entry, instancemetadata instancemet
roleassignments: authorization.NewRoleAssignmentsClient(instancemetadata.SubscriptionID(), armAuthorizer),
}
d.prod, err = newProd(ctx, log, instancemetadata, armClientAuthorizer, adminClientAuthorizer)
d.prod, err = newProd(ctx, log, instancemetadata)
if err != nil {
return nil, err
}
@ -151,6 +151,12 @@ func newDev(ctx context.Context, log *logrus.Entry, instancemetadata instancemet
return d, nil
}
func (d *dev) InitializeAuthorizers() error {
d.armClientAuthorizer = clientauthorizer.NewAll()
d.adminClientAuthorizer = clientauthorizer.NewAll()
return nil
}
func (d *dev) DatabaseName() string {
return os.Getenv("DATABASE_NAME")
}

17
pkg/env/env.go поставляемый
Просмотреть файл

@ -7,7 +7,6 @@ import (
"context"
"crypto/rsa"
"crypto/x509"
"fmt"
"net"
"os"
"strings"
@ -22,6 +21,7 @@ import (
type Interface interface {
instancemetadata.InstanceMetadata
InitializeAuthorizers() error
ArmClientAuthorizer() clientauthorizer.ClientAuthorizer
AdminClientAuthorizer() clientauthorizer.ClientAuthorizer
ClustersGenevaLoggingConfigVersion() string
@ -44,7 +44,7 @@ type Interface interface {
func NewEnv(ctx context.Context, log *logrus.Entry) (Interface, error) {
if strings.ToLower(os.Getenv("RP_MODE")) == "development" {
log.Warn("running in development mode")
return newDev(ctx, log, instancemetadata.NewDev(), clientauthorizer.NewAll(), clientauthorizer.NewAll())
return newDev(ctx, log, instancemetadata.NewDev())
}
im, err := instancemetadata.NewProd()
@ -54,17 +54,8 @@ func NewEnv(ctx context.Context, log *logrus.Entry) (Interface, error) {
if strings.ToLower(os.Getenv("RP_MODE")) == "int" {
log.Warn("running in int mode")
return newInt(ctx, log, im, clientauthorizer.NewARM(log), clientauthorizer.NewAdmin(log))
return newInt(ctx, log, im)
}
for _, key := range []string{
"MDM_ACCOUNT",
"MDM_NAMESPACE",
} {
if _, found := os.LookupEnv(key); !found {
return nil, fmt.Errorf("environment variable %q unset", key)
}
}
return newProd(ctx, log, im, clientauthorizer.NewARM(log), clientauthorizer.NewAdmin(log))
return newProd(ctx, log, im)
}

5
pkg/env/int.go поставляемый
Просмотреть файл

@ -8,12 +8,11 @@ import (
"github.com/sirupsen/logrus"
"github.com/Azure/ARO-RP/pkg/util/clientauthorizer"
"github.com/Azure/ARO-RP/pkg/util/instancemetadata"
)
func newInt(ctx context.Context, log *logrus.Entry, instancemetadata instancemetadata.InstanceMetadata, armClientAuthorizer, adminClientAuthorizer clientauthorizer.ClientAuthorizer) (*prod, error) {
p, err := newProd(ctx, log, instancemetadata, armClientAuthorizer, adminClientAuthorizer)
func newInt(ctx context.Context, log *logrus.Entry, instancemetadata instancemetadata.InstanceMetadata) (*prod, error) {
p, err := newProd(ctx, log, instancemetadata)
if err != nil {
return nil, err

27
pkg/env/prod.go поставляемый
Просмотреть файл

@ -10,6 +10,7 @@ import (
"encoding/base64"
"fmt"
"net"
"os"
"strings"
"time"
@ -52,23 +53,25 @@ type prod struct {
clustersGenevaLoggingPrivateKey *rsa.PrivateKey
clustersGenevaLoggingConfigVersion string
clustersGenevaLoggingEnvironment string
log *logrus.Entry
}
func newProd(ctx context.Context, log *logrus.Entry, instancemetadata instancemetadata.InstanceMetadata, armClientAuthorizer, adminClientAuthorizer clientauthorizer.ClientAuthorizer) (*prod, error) {
func newProd(ctx context.Context, log *logrus.Entry, instancemetadata instancemetadata.InstanceMetadata) (*prod, error) {
kvAuthorizer, err := auth.NewAuthorizerFromEnvironmentWithResource(azure.PublicCloud.ResourceIdentifiers.KeyVault)
if err != nil {
return nil, err
}
p := &prod{
InstanceMetadata: instancemetadata,
armClientAuthorizer: armClientAuthorizer,
adminClientAuthorizer: adminClientAuthorizer,
InstanceMetadata: instancemetadata,
keyvault: basekeyvault.New(kvAuthorizer),
clustersGenevaLoggingEnvironment: "DiagnosticsProd",
clustersGenevaLoggingConfigVersion: "2.1",
log: log,
}
rpAuthorizer, err := auth.NewAuthorizerFromEnvironment()
@ -116,6 +119,22 @@ func newProd(ctx context.Context, log *logrus.Entry, instancemetadata instanceme
return p, nil
}
func (p *prod) InitializeAuthorizers() error {
p.armClientAuthorizer = clientauthorizer.NewARM(p.log)
adminClientAuthorizer, err := clientauthorizer.NewAdmin(
p.log,
"/etc/aro-rp/admin-ca-bundle.pem",
os.Getenv("ADMIN_API_CLIENT_CERT_COMMON_NAME"),
)
if err != nil {
return err
}
p.adminClientAuthorizer = adminClientAuthorizer
return nil
}
func (p *prod) ArmClientAuthorizer() clientauthorizer.ClientAuthorizer {
return p.armClientAuthorizer
}

Просмотреть файл

@ -5,23 +5,90 @@ package clientauthorizer
import (
"crypto/tls"
"crypto/x509"
"fmt"
"io/ioutil"
"github.com/sirupsen/logrus"
)
type admin struct {
log *logrus.Entry
// NewAdmin creates a new instance of ClientAuthorizer to be used with Admin API.
// This authorizer allows connections only if they
// contain a valid client certificate signed by `caBundlePath` and
// the client certificate's CommonName equals `clientCertCommonName`.
func NewAdmin(log *logrus.Entry, caBundlePath, clientCertCommonName string) (ClientAuthorizer, error) {
authorizer := &admin{
clientCertCommonName: clientCertCommonName,
log: log,
readFile: ioutil.ReadFile,
}
err := authorizer.readCABundle(caBundlePath)
if err != nil {
return nil, err
}
return authorizer, nil
}
func NewAdmin(log *logrus.Entry) ClientAuthorizer {
return &admin{
log: log,
type admin struct {
roots *x509.CertPool
clientCertCommonName string
log *logrus.Entry
readFile func(filename string) ([]byte, error)
}
func (a *admin) readCABundle(caBundlePath string) error {
caBundle, err := a.readFile(caBundlePath)
if err != nil {
return err
}
roots := x509.NewCertPool()
ok := roots.AppendCertsFromPEM(caBundle)
if !ok {
return fmt.Errorf("can not decode admin CA bundle from %s", caBundlePath)
}
a.roots = roots
return nil
}
func (a *admin) IsAuthorized(cs *tls.ConnectionState) bool {
a.log.Print("Admin auth is not implemented yet")
return false
if a.roots == nil {
// Should never happen
a.log.Error("no CA certificate")
return false
}
if cs == nil || len(cs.PeerCertificates) == 0 {
a.log.Debug("no certificate present for the connection")
return false
}
verifyOpts := x509.VerifyOptions{
Roots: a.roots,
Intermediates: x509.NewCertPool(),
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
}
for _, cert := range cs.PeerCertificates[1:] {
verifyOpts.Intermediates.AddCert(cert)
}
_, err := cs.PeerCertificates[0].Verify(verifyOpts)
if err != nil {
a.log.Debug(err)
return false
}
if cs.PeerCertificates[0].Subject.CommonName != a.clientCertCommonName {
a.log.Debug("unexpected common name in the admin API client certificate")
return false
}
return true
}
func (a *admin) IsReady() bool {

Просмотреть файл

@ -0,0 +1,223 @@
package clientauthorizer
// Copyright (c) Microsoft Corporation.
// Licensed under the Apache License 2.0.
import (
"crypto/tls"
"errors"
"testing"
"github.com/sirupsen/logrus"
utiltls "github.com/Azure/ARO-RP/pkg/util/tls"
)
func TestAdminClientAuthorizer(t *testing.T) {
caBundlePath := "/fake/path/to/ca/cert.pem"
log := logrus.NewEntry(logrus.StandardLogger())
validCaKey, validCaCerts, err := utiltls.GenerateKeyAndCertificate("validca", nil, nil, true, false)
if err != nil {
t.Fatal(err)
}
for _, tt := range []struct {
name string
cs func() (*tls.ConnectionState, error)
want bool
}{
{
name: "allow: single valid client certificate",
want: true,
cs: func() (*tls.ConnectionState, error) {
_, validSingleClientCert, err := utiltls.GenerateKeyAndCertificate("validclient", validCaKey, validCaCerts[0], false, true)
if err != nil {
return nil, err
}
return &tls.ConnectionState{
PeerCertificates: validSingleClientCert,
}, nil
},
},
{
name: "allow: valid client certificate with intermediates",
want: true,
cs: func() (*tls.ConnectionState, error) {
validIntermediateCaKey, validIntermediateCaCerts, err := utiltls.GenerateKeyAndCertificate("valid-intermediate-ca", validCaKey, validCaCerts[0], true, false)
if err != nil {
return nil, err
}
_, validCertWithIntermediates, err := utiltls.GenerateKeyAndCertificate("validclient", validIntermediateCaKey, validIntermediateCaCerts[0], false, true)
if err != nil {
return nil, err
}
validCertWithIntermediates = append(validCertWithIntermediates, validIntermediateCaCerts...)
return &tls.ConnectionState{
PeerCertificates: validCertWithIntermediates,
}, nil
},
},
{
name: "deny: valid certificate with unexpected common name",
cs: func() (*tls.ConnectionState, error) {
_, invalidCommonNameClientCert, err := utiltls.GenerateKeyAndCertificate("invalidclient", validCaKey, validCaCerts[0], false, true)
if err != nil {
return nil, err
}
return &tls.ConnectionState{
PeerCertificates: invalidCommonNameClientCert,
}, nil
},
},
{
name: "deny: certificate with unexpected key usage",
cs: func() (*tls.ConnectionState, error) {
_, invalidKeyUsagesCert, err := utiltls.GenerateKeyAndCertificate("validclient", validCaKey, validCaCerts[0], false, false)
if err != nil {
return nil, err
}
return &tls.ConnectionState{
PeerCertificates: invalidKeyUsagesCert,
}, nil
},
},
{
name: "deny: matching common name, but unexpected ca",
cs: func() (*tls.ConnectionState, error) {
invalidCaKey, invalidCaCerts, err := utiltls.GenerateKeyAndCertificate("invalidca", nil, nil, true, false)
if err != nil {
return nil, err
}
_, invalidSigningCa, err := utiltls.GenerateKeyAndCertificate("validclient", invalidCaKey, invalidCaCerts[0], false, true)
if err != nil {
return nil, err
}
return &tls.ConnectionState{
PeerCertificates: invalidSigningCa,
}, nil
},
},
{
name: "deny: connection without client certificates",
cs: func() (*tls.ConnectionState, error) {
return &tls.ConnectionState{}, nil
},
},
{
name: "deny: nil connection state",
cs: func() (*tls.ConnectionState, error) {
return nil, nil
},
},
} {
t.Run(tt.name, func(t *testing.T) {
adminAuthorizer := &admin{
clientCertCommonName: "validclient",
log: log,
readFile: func(filename string) ([]byte, error) {
if filename != caBundlePath {
t.Fatal(filename)
return nil, errors.New("noop")
}
return utiltls.CertAsBytes(validCaCerts...)
},
}
err := adminAuthorizer.readCABundle(caBundlePath)
if err != nil {
t.Fatal(err)
}
cs, err := tt.cs()
if err != nil {
t.Error(err)
}
result := adminAuthorizer.IsAuthorized(cs)
if result != tt.want {
t.Error(result)
}
})
}
}
func TestAdminClientAuthorizerReadCABundle(t *testing.T) {
validCaKey, validCaCerts, err := utiltls.GenerateKeyAndCertificate("validca", nil, nil, true, false)
if err != nil {
t.Fatal(err)
}
_, validClientCert, err := utiltls.GenerateKeyAndCertificate("validclient", validCaKey, validCaCerts[0], false, true)
if err != nil {
t.Fatal(err)
}
cs := &tls.ConnectionState{PeerCertificates: validClientCert}
for _, tt := range []struct {
name string
readFile func(string) ([]byte, error)
want bool
}{
{
name: "valid ca cert",
readFile: func(string) ([]byte, error) {
return utiltls.CertAsBytes(validCaCerts...)
},
want: true,
},
{
name: "error reading ca cert file",
readFile: func(string) ([]byte, error) {
return nil, errors.New("noop")
},
},
{
name: "error decoding ca cert file",
readFile: func(string) ([]byte, error) {
return []byte("invalid-ca-cert"), nil
},
},
} {
t.Run(tt.name, func(t *testing.T) {
adminAuthorizer := &admin{
clientCertCommonName: "validclient",
log: logrus.NewEntry(logrus.StandardLogger()),
readFile: tt.readFile,
}
if adminAuthorizer.IsAuthorized(cs) {
t.Error("expected deny before the readCABundle call")
}
readCABundleErr := adminAuthorizer.readCABundle("/fake/path/to/ca/cert.pem")
IsAuthorized := adminAuthorizer.IsAuthorized(cs)
if tt.want {
if readCABundleErr != nil {
t.Error(readCABundleErr)
}
if !IsAuthorized {
t.Error("expected to allow")
}
} else {
if readCABundleErr == nil {
t.Error("expected an error")
}
if IsAuthorized {
t.Error("expected deny")
}
}
})
}
}