Skip to content

feat: add configurable certificate mount path support #361

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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
89 changes: 89 additions & 0 deletions CUSTOM_MOUNT_PATH.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Autocert Configuration

## Configurable Mount Path

By default, autocert mounts certificates at `/var/run/autocert.step.sm/`. You can now customize this path using annotations.

### Usage

Add the `autocert.step.sm/mount-path` annotation to your pod:

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
spec:
template:
metadata:
annotations:
autocert.step.sm/name: my-app.default.svc.cluster.local
autocert.step.sm/mount-path: /custom/cert/path
spec:
containers:
- name: app
image: my-app:latest
```
## Default Behavior

Without annotation: /var/run/autocert.step.sm/
With annotation: Your specified custom path

## File Structure
Certificates will be available at:

<mount-path>/site.crt
<mount-path>/site.key
<mount-path>/root.crt

### Important: Custom Controller Image Required
Note: This configurable mount path feature requires an updated controller image that is not yet available in the official repository. To use this feature, you need to:
## 1 Build Custom Controller Image
Since the original YAML files haven't been updated with the new images, you'll need to build and use custom Docker images with the updated code:
```bash
docker build -t your-registry/autocert-controller:custom -f controller/Dockerfile .
```
## 2 Load image to cluster
```bash
#for minikube:
minikube image load autocert-controller:custom

#for kind
kind load docker-image autocert-controller:custom --name <your cluster name>
```
## 3 Update Controller Deployment
Update the autocert controller deployment to use your custom image:

```bash
#restart deployment(if needed)
kubectl rollout restart deployment/<your-deployment-name>
#For local clusters (minikube, kind, Docker Desktop):
kubectl patch deployment autocert -n step -p '{"spec":{"template":{"spec":{"containers":[{"name":"autocert","image":"autocert-controller:custom","imagePullPolicy":"Never"}]}}}}'

#For remote clusters using registry
kubectl patch deployment autocert -n step -p '{"spec":{"template":{"spec":{"containers":[{"name":"autocert","image":"your-registry/autocert-controller:custom"}]}}}}'
```
## 4 Verify Deployment
Check that the controller is running with the new image:

```bash
# Check deployment status
kubectl get deployment autocert -n step

# Check pod is running
kubectl get pods -n step -l app=autocert

# Verify the new image is being used
kubectl describe deployment autocert -n step | grep Image
```

# 5 Verification
After updating the controller image, verify the feature works by:
Deploying a pod with the custom mount path annotation
Checking that certificates are mounted at your specified path
Verifying the application can access certificates at the new location

```bash
# Check if certificates are at custom path
kubectl exec -it <your-pod> -- ls -la /custom/cert/path/
```
73 changes: 65 additions & 8 deletions controller/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"io"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"unicode"
Expand Down Expand Up @@ -45,6 +46,7 @@ const (
sansAnnotationKey = "autocert.step.sm/sans"
ownerAnnotationKey = "autocert.step.sm/owner"
modeAnnotationKey = "autocert.step.sm/mode"
mountPathAnnotationKey = "autocert.step.sm/mount-path"
volumeMountPath = "/var/run/autocert.step.sm"
tokenSecretKey = "token"
//nolint:gosec // not a secret
Expand All @@ -59,6 +61,7 @@ type Config struct {
LogFormat string `yaml:"logFormat"`
CaURL string `yaml:"caUrl"`
CertLifetime string `yaml:"certLifetime"`
CertMountPath string `yaml:"certMountPath"`
Bootstrapper corev1.Container `yaml:"bootstrapper"`
Renewer corev1.Container `yaml:"renewer"`
CertsVolume corev1.Volume `yaml:"certsVolume"`
Expand Down Expand Up @@ -119,6 +122,14 @@ func (c Config) GetProvisionerPasswordPath() string {
return "/home/step/password/password"
}

// GetCertMountPath returns the certificate mount path, defaults to "/var/run/autocert.step.sm"
func (c Config) GetCertMountPath() string {
if c.CertMountPath != "" {
return c.CertMountPath
}
return "/var/run/autocert.step.sm"
}

// PatchOperation represents a RFC6902 JSONPatch Operation
type PatchOperation struct {
Op string `json:"op"`
Expand Down Expand Up @@ -258,7 +269,7 @@ func createTokenSecret(prefix, namespace, token string) (string, error) {
// mkBootstrapper generates a bootstrap container based on the template defined in Config. It
// generates a new bootstrap token and mounts it, along with other required configuration, as
// environment variables in the returned bootstrap container.
func mkBootstrapper(config *Config, podName, commonName, duration, owner, mode, namespace string, sans []string, provisioner *ca.Provisioner) (corev1.Container, error) {
func mkBootstrapper(config *Config, podName, commonName, duration, owner, mode, namespace string, sans []string, provisioner *ca.Provisioner, mountPath string) (corev1.Container, error) {
b := config.Bootstrapper

token, err := provisioner.Token(commonName, sans...)
Expand Down Expand Up @@ -332,13 +343,32 @@ func mkBootstrapper(config *Config, podName, commonName, duration, owner, mode,
corev1.EnvVar{
Name: "CLUSTER_DOMAIN",
Value: config.ClusterDomain,
},
corev1.EnvVar{
Name: "CRT",
Value: filepath.Join(mountPath, "site.crt"),
},
corev1.EnvVar{
Name: "KEY",
Value: filepath.Join(mountPath, "site.key"),
},
corev1.EnvVar{
Name: "STEP_ROOT",
Value: filepath.Join(mountPath, "root.crt"),
})

// Update volume mounts
for i := range b.VolumeMounts {
if b.VolumeMounts[i].Name == config.CertsVolume.Name {
b.VolumeMounts[i].MountPath = mountPath
}
}

return b, nil
}

// mkRenewer generates a new renewer based on the template provided in Config.
func mkRenewer(config *Config, podName, commonName, namespace string) corev1.Container {
func mkRenewer(config *Config, podName, commonName, namespace string, mountPath string) corev1.Container {
r := config.Renewer
r.Env = append(r.Env,
corev1.EnvVar{
Expand All @@ -360,7 +390,27 @@ func mkRenewer(config *Config, podName, commonName, namespace string) corev1.Con
corev1.EnvVar{
Name: "CLUSTER_DOMAIN",
Value: config.ClusterDomain,
},
corev1.EnvVar{
Name: "CRT",
Value: filepath.Join(mountPath, "site.crt"),
},
corev1.EnvVar{
Name: "KEY",
Value: filepath.Join(mountPath, "site.key"),
},
corev1.EnvVar{
Name: "STEP_ROOT",
Value: filepath.Join(mountPath, "root.crt"),
})

// Update volume mounts
for i := range r.VolumeMounts {
if r.VolumeMounts[i].Name == config.CertsVolume.Name {
r.VolumeMounts[i].MountPath = mountPath
}
}

return r
}

Expand Down Expand Up @@ -414,10 +464,10 @@ func addVolumes(existing, nu []corev1.Volume, path string) (ops []PatchOperation
return ops
}

func addCertsVolumeMount(volumeName string, containers []corev1.Container, containerType string, first bool) (ops []PatchOperation) {
func addCertsVolumeMount(volumeName string, containers []corev1.Container, containerType string, first bool, mountPath string) (ops []PatchOperation) {
volumeMount := corev1.VolumeMount{
Name: volumeName,
MountPath: volumeMountPath,
MountPath: mountPath,
ReadOnly: true,
}

Expand Down Expand Up @@ -499,8 +549,15 @@ func patch(pod *corev1.Pod, namespace string, config *Config, provisioner *ca.Pr
duration := annotations[durationWebhookStatusKey]
owner := annotations[ownerAnnotationKey]
mode := annotations[modeAnnotationKey]
renewer := mkRenewer(config, name, commonName, namespace)
bootstrapper, err := mkBootstrapper(config, name, commonName, duration, owner, mode, namespace, sans, provisioner)

mountPath := config.GetCertMountPath()

if podMountPath := annotations[mountPathAnnotationKey]; podMountPath != "" {
mountPath = podMountPath
}

renewer := mkRenewer(config, name, commonName, namespace, mountPath)
bootstrapper, err := mkBootstrapper(config, name, commonName, duration, owner, mode, namespace, sans, provisioner, mountPath)
if err != nil {
return nil, err
}
Expand All @@ -516,8 +573,8 @@ func patch(pod *corev1.Pod, namespace string, config *Config, provisioner *ca.Pr
ops = append(ops, addContainers(pod.Spec.InitContainers, []corev1.Container{bootstrapper}, "/spec/initContainers")...)
}

ops = append(ops, addCertsVolumeMount(config.CertsVolume.Name, pod.Spec.Containers, "containers", false)...)
ops = append(ops, addCertsVolumeMount(config.CertsVolume.Name, pod.Spec.InitContainers, "initContainers", first)...)
ops = append(ops, addCertsVolumeMount(config.CertsVolume.Name, pod.Spec.Containers, "containers", false, mountPath)...)
ops = append(ops, addCertsVolumeMount(config.CertsVolume.Name, pod.Spec.InitContainers, "initContainers", first, mountPath)...)
if !bootstrapperOnly {
ops = append(ops, addContainers(pod.Spec.Containers, []corev1.Container{renewer}, "/spec/containers")...)
}
Expand Down
8 changes: 6 additions & 2 deletions controller/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,25 +83,29 @@ func Test_mkRenewer(t *testing.T) {
podName string
commonName string
namespace string
mountPath string
}
tests := []struct {
name string
args args
want corev1.Container
}{
{"ok", args{&Config{CaURL: "caURL", ClusterDomain: "clusterDomain"}, "podName", "commonName", "namespace"}, corev1.Container{
{"ok", args{&Config{CaURL: "caURL", ClusterDomain: "clusterDomain"}, "podName", "commonName", "namespace", "/var/run/autocert.step.sm"}, corev1.Container{
Env: []corev1.EnvVar{
{Name: "STEP_CA_URL", Value: "caURL"},
{Name: "COMMON_NAME", Value: "commonName"},
{Name: "POD_NAME", Value: "podName"},
{Name: "NAMESPACE", Value: "namespace"},
{Name: "CLUSTER_DOMAIN", Value: "clusterDomain"},
{Name: "CRT", Value: "/var/run/autocert.step.sm/site.crt"},
{Name: "KEY", Value: "/var/run/autocert.step.sm/site.key"},
{Name: "STEP_ROOT", Value: "/var/run/autocert.step.sm/root.crt"},
},
}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := mkRenewer(tt.args.config, tt.args.podName, tt.args.commonName, tt.args.namespace); !reflect.DeepEqual(got, tt.want) {
if got := mkRenewer(tt.args.config, tt.args.podName, tt.args.commonName, tt.args.namespace, tt.args.mountPath); !reflect.DeepEqual(got, tt.want) {
t.Errorf("mkRenewer() = %v, want %v", got, tt.want)
}
})
Expand Down