Skip to content

Commit

Permalink
Add a Client Trust Anchor
Browse files Browse the repository at this point in the history
We're using RFC8705 to issue oauth2 access tokens to services (e.g.
kubernetes) so they can talk to other APIs (e.g. region).  This requires
the use of mTLS, and for that we need a trust anchor to pass to the TLS
termination. On top of all that we need to rationalize client TLS
handling, remove all the identity specific bits from core - it's just a
pain in arse, allow reconcilers to register and use CLI flags.
  • Loading branch information
spjmurray committed Aug 15, 2024
1 parent 5c514b6 commit 7eb40fd
Show file tree
Hide file tree
Showing 17 changed files with 304 additions and 372 deletions.
4 changes: 2 additions & 2 deletions charts/core/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ description: A Helm chart for deploying Unikorn Core

type: application

version: v0.1.64
appVersion: v0.1.64
version: v0.1.65
appVersion: v0.1.65

icon: https://assets.unikorn-cloud.org/images/logos/dark-on-light/icon.svg

Expand Down
22 changes: 22 additions & 0 deletions charts/core/templates/client-certificate.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{{- if (and .Values.clientCA .Values.clientCA.enabled .Values.clientCA.generate) }}
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: unikorn-client-ca
namespace: {{ .Values.certManager.namespace }}
labels:
{{- include "unikorn.labels" . | nindent 4 }}
spec:
issuerRef:
group: cert-manager.io
kind: Issuer
name: unikorn-self-signed-issuer
privateKey:
algorithm: RSA
encoding: PKCS8
size: 4096
secretName: unikorn-client-ca
isCA: true
commonName: Unikorn Client CA
duration: 87600h
{{- end }}
11 changes: 11 additions & 0 deletions charts/core/templates/client-clusterissuer.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{{- if (and .Values.ca .Values.ca.enabled) }}
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: unikorn-client-issuer
labels:
{{- include "unikorn.labels" . | nindent 4 }}
spec:
ca:
secretName: unikorn-client-ca
{{- end }}
15 changes: 14 additions & 1 deletion charts/core/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,18 @@ ca:
# If generate is false, then you must specify a certificate and key, which can be
# sourced from mkcert which will automatically install it in the system trust store.
# These must be base64 encoded strings.
# certificate: SSBhbSBjb21wbGV0ZSBub25zZW5zZS4gIFRoYW5rIHlvdSBmb3IgcmVhZGluZyB0aGlzLiAgR2V0IGEgbGlmZSE=
# certificate: SSBhbSBjb21wbGV0ZSBub25zZW5zZS4gIFRoYW5rIHlvdSBmb3IgcmVhZGluZyB0aGlzLiAgR2V0IGEgbGlmZSE=
# privateKey: SSBhbSBjb21wbGV0ZSBub25zZW5zZS4gIFRoYW5rIHlvdSBmb3IgcmVhZGluZyB0aGlzLiAgR2V0IGEgbGlmZSE=

# Unikorn uses mTLS for credentialless authentication between componets. This is
# only used in asynchronous controllers where a user access token is not availabile.
clientCA:
# Enable CA and issuer creation.
enabled: true

# Generate a self signed CA.
# This is typically used at a single site to act as the trust root.
# You will need to (somehow) distribute this to other sites so that services
# can issue certificates as the CA is rotated. The other option is to just
# issue them here at the root and distribute then to the services themselves.
generate: true
35 changes: 0 additions & 35 deletions pkg/authorization/accesstoken/context.go

This file was deleted.

35 changes: 0 additions & 35 deletions pkg/authorization/userinfo/context.go

This file was deleted.

23 changes: 0 additions & 23 deletions pkg/authorization/userinfo/types.go

This file was deleted.

163 changes: 163 additions & 0 deletions pkg/client/http.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
/*
Copyright 2024 the Unikorn Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package client

import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"

"github.com/spf13/pflag"

"github.com/unikorn-cloud/core/pkg/errors"

corev1 "k8s.io/api/core/v1"

"sigs.k8s.io/controller-runtime/pkg/client"
)

// HTTPOptions are generic options for HTTP clients.
type HTTPOptions struct {
// service determines the CLI flag prefix.
service string
// host is the identity Host name.
host string
// secretNamespace tells us where to source the CA secret.
secretNamespace string
// secretName is the root CA secret of the identity endpoint.
secretName string
}

func NewHTTPOptions(service string) *HTTPOptions {
return &HTTPOptions{
service: service,
}
}

func (o *HTTPOptions) Host() string {
return o.host
}

// AddFlags adds the options to the CLI flags.
func (o *HTTPOptions) AddFlags(f *pflag.FlagSet) {
f.StringVar(&o.host, o.service+"-host", "", "Identity endpoint URL.")
f.StringVar(&o.secretNamespace, o.service+"-ca-secret-namespace", "", "Identity endpoint CA certificate secret namespace.")
f.StringVar(&o.secretName, o.service+"-ca-secret-name", "", "Identity endpoint CA certificate secret.")
}

// ApplyTLSConfig adds CA certificates to the TLS configuration if one is specified.
func (o *HTTPOptions) ApplyTLSConfig(ctx context.Context, cli client.Client, config *tls.Config) error {
if o.secretName == "" {
return nil
}

secret := &corev1.Secret{}

if err := cli.Get(ctx, client.ObjectKey{Namespace: o.secretNamespace, Name: o.secretName}, secret); err != nil {
return err
}

if secret.Type != corev1.SecretTypeTLS {
return fmt.Errorf("%w: issuer CA not of type kubernetes.io/tls", errors.ErrSecretFormatError)
}

cert, ok := secret.Data[corev1.TLSCertKey]
if !ok {
return fmt.Errorf("%w: issuer CA missing tls.crt", errors.ErrSecretFormatError)
}

certPool := x509.NewCertPool()

if ok := certPool.AppendCertsFromPEM(cert); !ok {
return fmt.Errorf("%w: failed to load identity CA certificate", errors.ErrSecretFormatError)
}

config.RootCAs = certPool

return nil
}

// HTTPClientOptions allows generic options to be passed to all HTTP clients.
type HTTPClientOptions struct {
// secretNamespace tells us where to source the client certificate.
secretNamespace string
// secretName is the client certificate for the service.
secretName string
}

// AddFlags adds the options to the CLI flags.
func (o *HTTPClientOptions) AddFlags(f *pflag.FlagSet) {
f.StringVar(&o.secretNamespace, "client-certificate-namespace", o.secretNamespace, "Client certificate secret namespace.")
f.StringVar(&o.secretName, "client-certificate-name", o.secretName, "Client certificate secret name.")
}

// ApplyTLSClientConfig loads op a client certificate if one is configured and applies
// it to the provided TLS configuration.
func (o *HTTPClientOptions) ApplyTLSClientConfig(ctx context.Context, cli client.Client, config *tls.Config) error {
if o.secretNamespace == "" || o.secretName == "" {
return nil
}

secret := &corev1.Secret{}

if err := cli.Get(ctx, client.ObjectKey{Namespace: o.secretNamespace, Name: o.secretName}, secret); err != nil {
return err
}

if secret.Type != corev1.SecretTypeTLS {
return fmt.Errorf("%w: certificate not of type kubernetes.io/tls", errors.ErrSecretFormatError)
}

cert, ok := secret.Data[corev1.TLSCertKey]
if !ok {
return fmt.Errorf("%w: certificate missing tls.crt", errors.ErrSecretFormatError)
}

key, ok := secret.Data[corev1.TLSPrivateKeyKey]
if !ok {
return fmt.Errorf("%w: certifcate missing tls.key", errors.ErrSecretFormatError)
}

certificate, err := tls.X509KeyPair(cert, key)
if err != nil {
return err
}

config.Certificates = []tls.Certificate{
certificate,
}

return nil
}

// TLSClientConfig is a helper to create a TLS client configuration.
func TLSClientConfig(ctx context.Context, cli client.Client, options *HTTPOptions, clientOptions *HTTPClientOptions) (*tls.Config, error) {
config := &tls.Config{
MinVersion: tls.VersionTLS13,
}

if err := options.ApplyTLSConfig(ctx, cli, config); err != nil {
return nil, err
}

if err := clientOptions.ApplyTLSClientConfig(ctx, cli, config); err != nil {
return nil, err
}

return config, nil
}
4 changes: 0 additions & 4 deletions pkg/constants/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,6 @@ const (
// the region controller) that a resource owns.
CloudIdentityAnnotation = "unikorn-cloud.org/cloud-identity-id"

// IdentityCleanupReadyEventReason is used to identift asynchronous clean up
// routines.
IdentityCleanupReadyEventReason = "IdentityCleanupReady"

// Finalizer is applied to resources that need to be deleted manually
// and do other complex logic.
Finalizer = "unikorn"
Expand Down
6 changes: 6 additions & 0 deletions pkg/errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,10 @@ var (

// ErrKubeconfig is raised wne the Kubeconfig isn't correct.
ErrKubeconfig = errors.New("kubeconfig error")

// ErrSecretFormatError is returned when a secret doesn't meet the specification.
ErrSecretFormatError = errors.New("secret incorrectly formatted")

// ErrAPIStatus is returned when an API status code is unexpected.
ErrAPIStatus = errors.New("api status code unexpected")
)
Loading

0 comments on commit 7eb40fd

Please sign in to comment.