Skip to content

Commit

Permalink
feat(spin/certs): automate creating the default CA bundle secret
Browse files Browse the repository at this point in the history
Supersedes spinkube#184

Automate the creation of a secret for a default CA root certificate
bundle. A secret is created in each namespace that contains a spin
application. If a secret already exists with the name `spin-ca` it will
not be modified. This allows the default `spin-ca` secret to be
overridden by the user.

The embedded CA bundle is fetched from https://curl.se/ca/cacert.pem and
can be updated to the latest by running `go generate ./...`.

There is no owner reference on the secret which means it will persist
unless manually deleted. Meaning that if spin-operator is removed from
the cluster it will not be included in the cascading deletion.

Signed-off-by: Adam Reese <adam@reese.io>
  • Loading branch information
adamreese committed May 1, 2024
1 parent 679c6f3 commit ad32617
Show file tree
Hide file tree
Showing 8 changed files with 3,680 additions and 25 deletions.
10 changes: 10 additions & 0 deletions api/v1alpha1/spinappexecutor_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,16 @@ type ExecutorDeploymentConfig struct {
// RuntimeClassName is the runtime class name that should be used by pods created
// as part of a deployment.
RuntimeClassName string `json:"runtimeClassName"`

// CACertSecret specifies the name of the secret containing the CA
// certificates to be mounted to the deployment.
CACertSecret string `json:"caCertSecret,omitempty"`

// InstallDefaultCACerts specifies whether the default CA
// certificate bundle should be generated. When set a new secret
// will be created containing the certificates. If no secret name is
// defined in `CACertSecret` the secret name will be `spin-ca`.
InstallDefaultCACerts bool `json:"installDefaultCACerts,omitempty"`
}

// SpinAppExecutorStatus defines the observed state of SpinAppExecutor
Expand Down
12 changes: 12 additions & 0 deletions config/crd/bases/core.spinoperator.dev_spinappexecutors.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,18 @@ spec:
DeploymentConfig specifies how the deployment should be configured when
createDeployment is true.
properties:
caCertSecret:
description: |-
CACertSecret specifies the name of the secret containing the CA
certificates to be mounted to the deployment.
type: string
installDefaultCACerts:
description: |-
InstallDefaultCACerts specifies whether the default CA
certificate bundle should be generated. When set a new secret
will be created containing the certificates. If no secret name is
defined in `CACertSecret` the secret name will be `spin-ca`.
type: boolean
runtimeClassName:
description: |-
RuntimeClassName is the runtime class name that should be used by pods created
Expand Down
1 change: 1 addition & 0 deletions config/samples/spin-shim-executor.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ spec:
createDeployment: true
deploymentConfig:
runtimeClassName: wasmtime-spin-v2
installDefaultCACerts: true
3,581 changes: 3,581 additions & 0 deletions internal/cacerts/ca-certificates.crt

Large diffs are not rendered by default.

19 changes: 19 additions & 0 deletions internal/cacerts/cacerts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Package cacerts provides an embedded CA root certificates bundle.
package cacerts

// To update the default certificates run the following command in this
// directory
//
// curl -sfL https://curl.se/ca/cacert.pem -o ca-certificates.crt

import _ "embed"

//go:embed ca-certificates.crt
var caCertificates string

// CACertificates returns the default bundle of CA root certificates.
// The certificate bundle is under the MPL-2.0 licence from
// https://curl.se/ca/cacert.pem.
func CACertificates() string {
return caCertificates
}
30 changes: 15 additions & 15 deletions internal/controller/deployment.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ import (
"fmt"
"strings"

corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/intstr"

spinv1alpha1 "github.com/spinkube/spin-operator/api/v1alpha1"
"github.com/spinkube/spin-operator/internal/generics"
"github.com/spinkube/spin-operator/pkg/spinapp"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/intstr"
)

func constructRuntimeConfigSecretMount(_ctx context.Context, secretName string) (corev1.Volume, corev1.VolumeMount) {
Expand Down Expand Up @@ -39,19 +40,17 @@ func constructRuntimeConfigSecretMount(_ctx context.Context, secretName string)
return volume, volumeMount
}

func constructCASecretMount(_ctx context.Context, secretName string) (corev1.Volume, corev1.VolumeMount) {
func constructCASecretMount(_ context.Context, caSecretName string) (corev1.Volume, corev1.VolumeMount) {
volume := corev1.Volume{
Name: "spin-ca",
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{
SecretName: secretName,
SecretName: caSecretName,
Optional: ptr(true),
Items: []corev1.KeyToPath{
{
Key: "ca-certificates.crt",
Path: "ca-certificates.crt",
},
},
Items: []corev1.KeyToPath{{
Key: "ca-certificates.crt",
Path: "ca-certificates.crt",
}},
},
},
}
Expand All @@ -69,7 +68,7 @@ func constructCASecretMount(_ctx context.Context, secretName string) (corev1.Vol
// any required volume mounts. A generated runtime secret is mutually
// exclusive with a user-provided secret - this is to require _either_ a
// manual runtime-config or a generated one from the crd.
func ConstructVolumeMountsForApp(ctx context.Context, app *spinv1alpha1.SpinApp, generatedRuntimeSecret string, caSecretName string) ([]corev1.Volume, []corev1.VolumeMount, error) {
func ConstructVolumeMountsForApp(ctx context.Context, app *spinv1alpha1.SpinApp, generatedRuntimeSecret, caSecretName string) ([]corev1.Volume, []corev1.VolumeMount, error) {
volumes := []corev1.Volume{}
volumeMounts := []corev1.VolumeMount{}

Expand All @@ -93,10 +92,11 @@ func ConstructVolumeMountsForApp(ctx context.Context, app *spinv1alpha1.SpinApp,
volumes = append(volumes, app.Spec.Volumes...)
volumeMounts = append(volumeMounts, app.Spec.VolumeMounts...)

// TODO: eventually add runtime configuration for specifying the CA bundle to use.
caVolume, caVolumeMount := constructCASecretMount(ctx, caSecretName)
volumes = append(volumes, caVolume)
volumeMounts = append(volumeMounts, caVolumeMount)
if caSecretName != "" {
caVolume, caVolumeMount := constructCASecretMount(ctx, caSecretName)
volumes = append(volumes, caVolume)
volumeMounts = append(volumeMounts, caVolumeMount)
}

return volumes, volumeMounts, nil
}
Expand Down
13 changes: 7 additions & 6 deletions internal/controller/deployment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,16 @@ import (
"context"
"testing"

spinv1alpha1 "github.com/spinkube/spin-operator/api/v1alpha1"
"github.com/spinkube/spin-operator/pkg/spinapp"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/intstr"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"

spinv1alpha1 "github.com/spinkube/spin-operator/api/v1alpha1"
"github.com/spinkube/spin-operator/pkg/spinapp"
)

func minimalSpinApp() *spinv1alpha1.SpinApp {
Expand Down Expand Up @@ -61,8 +62,8 @@ func TestConstructVolumeMountsForApp_Contract(t *testing.T) {
app.Spec.RuntimeConfig.LoadFromSecret = ""
volumes, mounts, err := ConstructVolumeMountsForApp(context.Background(), app, "", "")
require.NoError(t, err)
require.Empty(t, volumes)
require.Empty(t, mounts)
require.Len(t, volumes, 0)
require.Len(t, mounts, 0)

// User provided runtime secret is ok
app = minimalSpinApp()
Expand All @@ -78,8 +79,8 @@ func TestConstructVolumeMountsForApp_Contract(t *testing.T) {
app.Spec.RuntimeConfig.LoadFromSecret = ""
volumes, mounts, err = ConstructVolumeMountsForApp(context.Background(), app, "gen-secret", "spin-ca")
require.NoError(t, err)
require.Len(t, volumes, 1)
require.Len(t, mounts, 1)
require.Len(t, volumes, 2)
require.Len(t, mounts, 2)
require.Equal(t, "gen-secret", volumes[0].VolumeSource.Secret.SecretName)
}

Expand Down
39 changes: 35 additions & 4 deletions internal/controller/spinapp_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"fmt"
"hash/adler32"

"github.com/pelletier/go-toml/v2"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
Expand All @@ -33,8 +34,8 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"

"github.com/pelletier/go-toml/v2"
spinv1alpha1 "github.com/spinkube/spin-operator/api/v1alpha1"
"github.com/spinkube/spin-operator/internal/cacerts"
"github.com/spinkube/spin-operator/internal/logging"
"github.com/spinkube/spin-operator/internal/runtimeconfig"
"github.com/spinkube/spin-operator/pkg/spinapp"
Expand Down Expand Up @@ -225,6 +226,29 @@ func (r *SpinAppReconciler) updateStatus(ctx context.Context, app *spinv1alpha1.
return nil
}

const defaultCASecretName = "spin-ca"

// ensureDefaultCASecret creates the default ca certificate bundle in the
// namespace of the app. Only one is required per namespace. The secret can be
// overridden by the cluster operator.
func (r *SpinAppReconciler) ensureDefaultCASecret(ctx context.Context, namespace string) error {
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: defaultCASecretName,
Namespace: namespace,
},
StringData: map[string]string{"ca-certificates.crt": cacerts.CACertificates()},
}

err := r.Client.Get(ctx, types.NamespacedName{Name: defaultCASecretName, Namespace: namespace}, secret)
if !apierrors.IsNotFound(err) { // secret is not not found
return nil
}

err = r.Client.Create(ctx, secret)
return client.IgnoreAlreadyExists(err)
}

// reconcileDeployment creates a deployment if one does not exist and reconciles it if it does.
func (r *SpinAppReconciler) reconcileDeployment(ctx context.Context, app *spinv1alpha1.SpinApp, config *spinv1alpha1.ExecutorDeploymentConfig) error {
log := logging.FromContext(ctx).WithValues("deployment", app.Name)
Expand Down Expand Up @@ -279,8 +303,15 @@ func (r *SpinAppReconciler) reconcileDeployment(ctx context.Context, app *spinv1
}
}

// TODO: make this configurable
caSecretName := "spin-ca"
var caSecretName string
if config.CACertSecret != "" {
caSecretName = config.CACertSecret
} else if config.InstallDefaultCACerts {
caSecretName = defaultCASecretName
if err := r.ensureDefaultCASecret(ctx, app.Namespace); err != nil {
return fmt.Errorf("unable to create default ca-certificate secret: %w", err)
}
}

desiredDeployment, err := constructDeployment(ctx, app, config, generatedRuntimeConfigSecretName, caSecretName, r.Scheme)
if err != nil {
Expand Down Expand Up @@ -349,7 +380,7 @@ func (r *SpinAppReconciler) deleteDeployment(ctx context.Context, app *spinv1alp

// constructDeployment builds an appsv1.Deployment based on the configuration of a SpinApp.
func constructDeployment(ctx context.Context, app *spinv1alpha1.SpinApp, config *spinv1alpha1.ExecutorDeploymentConfig,
generatedRuntimeConfigSecretName string, caSecretName string, scheme *runtime.Scheme) (*appsv1.Deployment, error) {
generatedRuntimeConfigSecretName, caSecretName string, scheme *runtime.Scheme) (*appsv1.Deployment, error) {
// TODO: Once we land admission webhooks write some validation to make
// replicas and enableAutoscaling mutually exclusive.
var replicas *int32
Expand Down

0 comments on commit ad32617

Please sign in to comment.