diff --git a/CUSTOM_MOUNT_PATH.md b/CUSTOM_MOUNT_PATH.md new file mode 100644 index 00000000..01444fe5 --- /dev/null +++ b/CUSTOM_MOUNT_PATH.md @@ -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: + +/site.crt +/site.key +/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 +``` +## 3 Update Controller Deployment +Update the autocert controller deployment to use your custom image: + +```bash +#restart deployment(if needed) +kubectl rollout restart deployment/ +#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 -- ls -la /custom/cert/path/ +``` \ No newline at end of file diff --git a/controller/main.go b/controller/main.go index 7c759bea..bc625ba6 100644 --- a/controller/main.go +++ b/controller/main.go @@ -10,6 +10,7 @@ import ( "io" "net/http" "os" + "path/filepath" "strings" "time" "unicode" @@ -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 @@ -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"` @@ -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"` @@ -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...) @@ -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{ @@ -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 } @@ -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, } @@ -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 } @@ -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")...) } diff --git a/controller/main_test.go b/controller/main_test.go index 1441bb00..19c7924f 100644 --- a/controller/main_test.go +++ b/controller/main_test.go @@ -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) } })