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

Allow to load trusted CAs from k8s secrets #60

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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