Skip to content
This repository has been archived by the owner on Apr 22, 2024. It is now read-only.

Commit

Permalink
Allow to load trusted CAs from k8s secrets
Browse files Browse the repository at this point in the history
  • Loading branch information
sergicastro committed Feb 26, 2024
1 parent bef473d commit 42156a4
Show file tree
Hide file tree
Showing 20 changed files with 659 additions and 217 deletions.
7 changes: 5 additions & 2 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,20 +27,22 @@ import (
"github.com/tetrateio/authservice-go/internal/k8s"
"github.com/tetrateio/authservice-go/internal/oidc"
"github.com/tetrateio/authservice-go/internal/server"
"github.com/tetrateio/authservice-go/internal/tls"
)

func main() {
var (
lifecycle = run.NewLifecycle()
configFile = &internal.LocalConfigFile{}
logging = internal.NewLogSystem(log.New(), &configFile.Config)
tlsPool = internal.NewTLSConfigPool(lifecycle.Context())
k8sClient = k8s.NewClientLoader(&configFile.Config)
tlsPool = tls.NewTLSConfigPool(lifecycle.Context(), k8sClient)
jwks = oidc.NewJWKSProvider(tlsPool)
sessions = oidc.NewSessionStoreFactory(&configFile.Config)
envoyAuthz = server.NewExtAuthZFilter(&configFile.Config, tlsPool, jwks, sessions)
authzServer = server.New(&configFile.Config, envoyAuthz.Register)
healthz = server.NewHealthServer(&configFile.Config)
secrets = k8s.NewSecretLoader(&configFile.Config)
secrets = k8s.NewSecretLoader(&configFile.Config, k8sClient)
)

configLog := run.NewPreRunner("config-log", func() error {
Expand All @@ -57,6 +59,7 @@ func main() {
lifecycle, // manage the lifecycle of the run.Services
configFile, // load the configuration
logging, // set up the logging system
k8sClient, // start the Kubernetes client
secrets, // load the secrets and update the configuration
configLog, // log the configuration
jwks, // start the JWKS provider
Expand Down
173 changes: 100 additions & 73 deletions config/gen/go/v1/oidc/config.pb.go

Large diffs are not rendered by default.

41 changes: 41 additions & 0 deletions config/gen/go/v1/oidc/config.pb.validate.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions config/v1/oidc/config.proto
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,10 @@ message OIDCConfig {
// The file path to the PEM-encoded certificate authority to trust when performing HTTPS calls to the OIDC Identity Provider.
// Optional.
string trusted_certificate_authority_file = 20;

// The Kubernetes secret that contains the PEM-encoded certificate authority to trust
// when performing HTTPS calls to the OIDC Identity Provider.
SecretReference trusted_certificate_authority_secret = 23;
}

// The duration between refreshes of the trusted certificate authority if `trusted_certificate_authority_file` is set.
Expand Down
2 changes: 1 addition & 1 deletion e2e/istio/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ include ../suite-certs.mk
include ../suite-k8s.mk

.PHONY: gen-certs
gen-certs: clean-certs ca/ca.authservice.internal certificate/http-echo.authservice.internal
gen-certs: clean-certs ca/ca.authservice.internal certificate/http-echo.authservice.internal certificate/keycloak.keycloak
@chmod -R a+r $(CERTS_DIR)

.PHONY: clean
Expand Down
9 changes: 7 additions & 2 deletions e2e/istio/cluster/manifests/authservice.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ data:
{
"oidc":
{
"configuration_uri": "http://keycloak.keycloak:8080/realms/master/.well-known/openid-configuration",
"configuration_uri": "https://keycloak.keycloak/realms/master/.well-known/openid-configuration",
"callback_uri": "https://http-echo.authservice.internal/callback",
"client_id": "authservice",
"client_secret_ref": {
Expand All @@ -163,7 +163,12 @@ data:
},
"redis_session_store_config": {
"server_uri": "redis://redis.redis.svc.cluster.local:6379"
}
},
"trusted_certificate_authority_secret": {
"namespace": "authservice",
"name": "keycloak-ca"
},
"trusted_certificate_authority_refresh_interval": "60s"
}
}
]
Expand Down
18 changes: 16 additions & 2 deletions e2e/istio/cluster/manifests/keycloak.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@ spec:
- port: 8080
targetPort: 8080
name: http-keycloak
protocol: TCP
- port: 443
targetPort: 9443
nodePort: 30001 # Expose it directly to the e2e tests
name: https
protocol: TCP
selector:
app: keycloak
Expand Down Expand Up @@ -66,10 +70,13 @@ spec:
image: quay.io/keycloak/keycloak:23.0.6
imagePullPolicy: IfNotPresent
args:
- "start-dev"
- start-dev
- --https-port=9443
- --https-certificate-file=/opt/keycloak/certs/tls.crt
- --https-certificate-key-file=/opt/keycloak/certs/tls.key
ports:
- name: keycloak
containerPort: 8080
containerPort: 9443
protocol: TCP
env:
- name: KEYCLOAK_ADMIN
Expand All @@ -81,6 +88,13 @@ spec:
periodSeconds: 5
tcpSocket:
port: 8080
volumeMounts:
- mountPath: /opt/keycloak/certs
name: keycloak-certs
volumes:
- name: keycloak-certs
secret:
secretName: keycloak-certs
---
apiVersion: batch/v1
kind: Job
Expand Down
2 changes: 1 addition & 1 deletion e2e/istio/istio_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func (i *IstioSuite) TestIstioEnforcement() {
// Map the keycloak cluster DNS name to the local address where the service is exposed
e2e.WithCustomAddressMappings(map[string]string{
"http-echo.authservice.internal:443": "localhost:30000",
"keycloak.keycloak:8080": "localhost:30001",
"keycloak.keycloak:443": "localhost:30001",
}),
)
i.Require().NoError(err)
Expand Down
60 changes: 60 additions & 0 deletions e2e/istio/suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,18 @@ package istio
import (
"context"
"fmt"
"os"
"os/exec"
"strings"
"testing"

"github.com/stretchr/testify/suite"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
applycorev1 "k8s.io/client-go/applyconfigurations/core/v1"
applymetav1 "k8s.io/client-go/applyconfigurations/meta/v1"
"k8s.io/client-go/kubernetes"
v1 "k8s.io/client-go/kubernetes/typed/core/v1"

"github.com/tetrateio/authservice-go/e2e"
)
Expand Down Expand Up @@ -80,6 +84,8 @@ func (i *IstioSuite) SetupSuite() {
i.installistio()
}

i.installKeycloakCerts()

i.T().Log("deploying the test services...")
for _, f := range testManifests {
i.MustApply(context.Background(), manifestsDir+"/"+f)
Expand All @@ -104,3 +110,57 @@ func (i *IstioSuite) istioInstalled(client kubernetes.Interface) bool {
_, err := client.CoreV1().Services("istio-system").Get(context.Background(), "istiod", metav1.GetOptions{})
return err == nil
}

// Install the Keycloak CA certificate in the cluster
func (i *IstioSuite) installKeycloakCerts() {
// load the Keycloak certificates
ca, err := os.ReadFile("certs/ca.crt")
i.Require().NoError(err)
cert, err := os.ReadFile("certs/keycloak.keycloak.crt")
i.Require().NoError(err)
key, err := os.ReadFile("certs/keycloak.keycloak.key")
i.Require().NoError(err)

// Create the secret with the Keycloak certificates in the "keycloak" namespace
i.applyNamespace("keycloak")
i.applySecret("keycloak-certs", "keycloak", corev1.SecretTypeTLS, map[string][]byte{"ca.crt": ca, "tls.crt": cert, "tls.key": key})

// Create the secret with the Keycloak CA in the "authservice" namespace
i.applyNamespace("authservice")
i.applySecret("keycloak-ca", "authservice", corev1.SecretTypeOpaque, map[string][]byte{"ca.crt": ca})
}

func (i *IstioSuite) applyNamespace(name string) {
k8sClient, err := v1.NewForConfig(i.Kubeconfig)
i.Require().NoError(err)

var (
namespaceKind = "Namespace"
namespaceAPI = "v1"
)

namespace := &applycorev1.NamespaceApplyConfiguration{
TypeMetaApplyConfiguration: applymetav1.TypeMetaApplyConfiguration{Kind: &namespaceKind, APIVersion: &namespaceAPI},
ObjectMetaApplyConfiguration: &applymetav1.ObjectMetaApplyConfiguration{Name: &name},
}
_, err = k8sClient.Namespaces().Apply(context.Background(), namespace, metav1.ApplyOptions{FieldManager: "e2e"})
i.Require().NoError(err)
}

func (i *IstioSuite) applySecret(name, namespace string, secretType corev1.SecretType, data map[string][]byte) {
k8sClient, err := v1.NewForConfig(i.Kubeconfig)
i.Require().NoError(err)

var (
secretKind = "Secret"
secretAPI = "v1"
)
secret := &applycorev1.SecretApplyConfiguration{
TypeMetaApplyConfiguration: applymetav1.TypeMetaApplyConfiguration{Kind: &secretKind, APIVersion: &secretAPI},
ObjectMetaApplyConfiguration: &applymetav1.ObjectMetaApplyConfiguration{Name: &name, Namespace: &namespace},
Data: data,
Type: &secretType,
}
_, err = k8sClient.Secrets(namespace).Apply(context.Background(), secret, metav1.ApplyOptions{FieldManager: "e2e"})
i.Require().NoError(err)
}
2 changes: 1 addition & 1 deletion e2e/keycloak/docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ services:
KEYCLOAK_ADMIN_PASSWORD: admin
ports:
- "9443:9443"
command: start-dev --https-port=9443 --https-certificate-file=/opt/keycloak/certs/host.docker.internal.crt --https-certificate-key-file=/opt/keycloak/certs/host.docker.internal.key
command: start-dev --https-port=9443 --https-certificate-file=/opt/keycloak/certs/host.docker.internal.crt --https-certificate-key-file=/opt/keycloak/certs/host.docker.internal.key
volumes:
- type: bind
source: certs
Expand Down
7 changes: 4 additions & 3 deletions internal/authz/oidc.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import (
"github.com/tetrateio/authservice-go/internal"
inthttp "github.com/tetrateio/authservice-go/internal/http"
"github.com/tetrateio/authservice-go/internal/oidc"
"github.com/tetrateio/authservice-go/internal/tls"
)

var (
Expand All @@ -53,7 +54,7 @@ var (
type oidcHandler struct {
log telemetry.Logger
config *oidcv1.OIDCConfig
tlsPool internal.TLSConfigPool
tlsPool tls.ConfigPool
jwks oidc.JWKSProvider
sessions oidc.SessionStoreFactory
sessionGen oidc.SessionGenerator
Expand All @@ -62,7 +63,7 @@ type oidcHandler struct {
}

// NewOIDCHandler creates a new OIDC implementation of the Handler interface.
func NewOIDCHandler(cfg *oidcv1.OIDCConfig, tlsPool internal.TLSConfigPool, jwks oidc.JWKSProvider,
func NewOIDCHandler(cfg *oidcv1.OIDCConfig, tlsPool tls.ConfigPool, jwks oidc.JWKSProvider,
sessions oidc.SessionStoreFactory, clock oidc.Clock, sessionGen oidc.SessionGenerator) (Handler, error) {

client, err := getHTTPClient(cfg, tlsPool)
Expand All @@ -86,7 +87,7 @@ func NewOIDCHandler(cfg *oidcv1.OIDCConfig, tlsPool internal.TLSConfigPool, jwks
}, nil
}

func getHTTPClient(cfg *oidcv1.OIDCConfig, tlsPool internal.TLSConfigPool) (*http.Client, error) {
func getHTTPClient(cfg *oidcv1.OIDCConfig, tlsPool tls.ConfigPool) (*http.Client, error) {
transport := http.DefaultTransport.(*http.Transport).Clone()

var err error
Expand Down
17 changes: 9 additions & 8 deletions internal/authz/oidc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,10 @@ import (
"google.golang.org/grpc/test/bufconn"

oidcv1 "github.com/tetrateio/authservice-go/config/gen/go/v1/oidc"
"github.com/tetrateio/authservice-go/internal"
inthttp "github.com/tetrateio/authservice-go/internal/http"
"github.com/tetrateio/authservice-go/internal/k8s"
"github.com/tetrateio/authservice-go/internal/oidc"
"github.com/tetrateio/authservice-go/internal/tls"
)

var (
Expand Down Expand Up @@ -202,7 +203,7 @@ func TestOIDCProcess(t *testing.T) {
clock := oidc.Clock{}
sessions := &mockSessionStoreFactory{store: oidc.NewMemoryStore(&clock, time.Hour, time.Hour)}
store := sessions.Get(basicOIDCConfig)
tlsPool := internal.NewTLSConfigPool(context.Background())
tlsPool := tls.NewTLSConfigPool(context.Background(), k8s.NewClientLoader(nil))
h, err := NewOIDCHandler(basicOIDCConfig, tlsPool, oidc.NewJWKSProvider(tlsPool), sessions, clock, oidc.NewStaticGenerator(newSessionID, newNonce, newState))
require.NoError(t, err)

Expand Down Expand Up @@ -879,7 +880,7 @@ func TestOIDCProcess(t *testing.T) {
func TestOIDCProcessWithFailingSessionStore(t *testing.T) {
store := &storeMock{delegate: oidc.NewMemoryStore(&oidc.Clock{}, time.Hour, time.Hour)}
sessions := &mockSessionStoreFactory{store: store}
tlsPool := internal.NewTLSConfigPool(context.Background())
tlsPool := tls.NewTLSConfigPool(context.Background(), k8s.NewClientLoader(nil))

jwkPriv, jwkPub := newKeyPair(t)
bytes, err := json.Marshal(newKeySet(jwkPub))
Expand Down Expand Up @@ -1033,7 +1034,7 @@ func TestOIDCProcessWithFailingJWKSProvider(t *testing.T) {
clock := oidc.Clock{}
sessions := &mockSessionStoreFactory{store: oidc.NewMemoryStore(&clock, time.Hour, time.Hour)}
store := sessions.Get(basicOIDCConfig)
tlsPool := internal.NewTLSConfigPool(context.Background())
tlsPool := tls.NewTLSConfigPool(context.Background(), k8s.NewClientLoader(nil))
h, err := NewOIDCHandler(basicOIDCConfig, tlsPool, funcJWKSProvider, sessions, clock, oidc.NewStaticGenerator(newSessionID, newNonce, newState))
require.NoError(t, err)

Expand Down Expand Up @@ -1240,7 +1241,7 @@ func TestEncodeTokensToHeaders(t *testing.T) {
}

sessions := &mockSessionStoreFactory{store: oidc.NewMemoryStore(&oidc.Clock{}, time.Hour, time.Hour)}
tlsPool := internal.NewTLSConfigPool(context.Background())
tlsPool := tls.NewTLSConfigPool(context.Background(), k8s.NewClientLoader(nil))

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand Down Expand Up @@ -1313,7 +1314,7 @@ func TestAreTokensExpired(t *testing.T) {
}

sessions := &mockSessionStoreFactory{store: oidc.NewMemoryStore(&oidc.Clock{}, time.Hour, time.Hour)}
tlsPool := internal.NewTLSConfigPool(context.Background())
tlsPool := tls.NewTLSConfigPool(context.Background(), k8s.NewClientLoader(nil))

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand Down Expand Up @@ -1348,15 +1349,15 @@ func TestLoadWellKnownConfig(t *testing.T) {

func TestLoadWellKnownConfigError(t *testing.T) {
clock := oidc.Clock{}
tlsPool := internal.NewTLSConfigPool(context.Background())
tlsPool := tls.NewTLSConfigPool(context.Background(), k8s.NewClientLoader(nil))
sessions := &mockSessionStoreFactory{store: oidc.NewMemoryStore(&clock, time.Hour, time.Hour)}
_, err := NewOIDCHandler(dynamicOIDCConfig, tlsPool, oidc.NewJWKSProvider(tlsPool), sessions, clock, oidc.NewStaticGenerator(newSessionID, newNonce, newState))
require.Error(t, err) // Fail to retrieve the dynamic config since the test server is not running
}

func TestNewOIDCHandler(t *testing.T) {
clock := oidc.Clock{}
tlsPool := internal.NewTLSConfigPool(context.Background())
tlsPool := tls.NewTLSConfigPool(context.Background(), k8s.NewClientLoader(nil))
sessions := &mockSessionStoreFactory{store: oidc.NewMemoryStore(&clock, time.Hour, time.Hour)}

tests := []struct {
Expand Down
Loading

0 comments on commit 42156a4

Please sign in to comment.