Skip to content
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

Run a custom script at renewal time #57

Draft
wants to merge 7 commits into
base: master
Choose a base branch
from
Draft
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
47 changes: 45 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,11 @@ By default the certificate, key and root will be owned by root and world-readabl
Use the `autocert.step.sm/owner` and `autocert.step.sm/mode` annotations to set the owner and permissions of the files.
The owner annotation requires user and group IDs rather than names because the images used by the containers that create and renew the certificates do not have the same user list as the main application containers.

To run a custom script at certificate renewal time, add an
`autocert.step.sm/renewalVolume` annotation, with the value the name of a volume
to mount in the renewal container. The volume should contain an executable
called `renew.sh`, which will be run by `step ca renew --exec`.


Let's deploy a [simple mTLS server](examples/hello-mtls/go/server/server.go)
named `hello-mtls.default.svc.cluster.local`:
Expand Down Expand Up @@ -161,7 +166,8 @@ root.crt site.crt site.key
We're done. Our container has a certificate, issued by our CA, which `autocert`
will automatically renew.

Now let's deploy another server with a `autocert.step.sm/duration`, `autocert.step.sm/owner` and `autocert.step.sm/mode`:
Now let's deploy another server with a `autocert.step.sm/duration`, `autocert.step.sm/owner`,
`autocert.step.sm/mode` and `autocert.step.sm/renewalVolume`:

```yaml
cat <<EOF | kubectl apply -f -
Expand All @@ -178,6 +184,7 @@ spec:
autocert.step.sm/duration: 1h
autocert.step.sm/owner: "999:999"
autocert.step.sm/mode: "0600"
autocert.step.sm/renewalVolume: "renewal"
labels: {app: hello-mtls-1h}
spec:
containers:
Expand All @@ -186,7 +193,40 @@ spec:
EOF
```

The container will have the certificates and key owned by user/group 999 with permission to read/write restricted to the owner, and the certificate duration will be valid for one hour and will be autorenewed:
Create a config map with a simple post-renewal script:

```yaml
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: ConfigMap
metadata:
name: renewal-script
data:
renew.sh: |
#!/bin/sh

echo "New certificate at $CRT"
EOF
```

And update the pod specification to include the volume:

```yaml
volumes:
- name: renewal
configMap:
name: renewal-script
defaultMode: "0777"
```

You may also need to add `shareProcessNamespace: true` to your pod specification
so the renewal script can see processes in other contianers (e.g., so the script
can restart nginx).

The container will have the certificates and key owned by user/group 999 with
permission to read/write restricted to the owner, and the certificate duration
will be valid for one hour and will be autorenewed. After the certificate and
key have been renewed, the renewal script will run.

```bash
$ export HELLO_MTLS_1H=$(kubectl get pods -l app=hello-mtls-1h -o jsonpath='{$.items[0].metadata.name}')
Expand All @@ -201,6 +241,9 @@ X.509v3 TLS Certificate (ECDSA P-256) [Serial: 3182...1140]
Provisioner: autocert [ID: A1lX...ty1Q]
Valid from: 2020-04-30T01:58:17Z
to: 2020-04-30T02:58:17Z
$ kubectl logs --follow $HELLO_MTLS-1H --container autocert-renewal
INFO: 2022/06/12 10:20:58 hello-mtls-1h.default.svc.cluster.local certificate renewed, next in 40m24s
Hello world!
```

Durations are specially useful if the `step-ca` provisioner is configured with a
Expand Down
15 changes: 13 additions & 2 deletions controller/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const (
sansAnnotationKey = "autocert.step.sm/sans"
ownerAnnotationKey = "autocert.step.sm/owner"
modeAnnotationKey = "autocert.step.sm/mode"
renewerVolumeAnnotationKey = "autocert.step.sm/renewerVolume"
volumeMountPath = "/var/run/autocert.step.sm"
tokenSecretKey = "token"
tokenSecretLabel = "autocert.step.sm/token"
Expand Down Expand Up @@ -330,7 +331,7 @@ func mkBootstrapper(config *Config, podName, commonName, duration, owner, mode,
}

// 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, renewerVolume string) corev1.Container {
r := config.Renewer
r.Env = append(r.Env, corev1.EnvVar{
Name: "STEP_CA_URL",
Expand All @@ -352,6 +353,15 @@ func mkRenewer(config *Config, podName, commonName, namespace string) corev1.Con
Name: "CLUSTER_DOMAIN",
Value: config.ClusterDomain,
})

if renewerVolume != "" {
r.VolumeMounts = append(r.VolumeMounts, corev1.VolumeMount{
Name: renewerVolume,
MountPath: "/renewer",
ReadOnly: true,
})
}

return r
}

Expand Down Expand Up @@ -490,7 +500,8 @@ 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)
renewerVolume := annotations[renewerVolumeAnnotationKey]
renewer := mkRenewer(config, name, commonName, namespace, renewerVolume)
bootstrapper, err := mkBootstrapper(config, name, commonName, duration, owner, mode, namespace, sans, provisioner)
if err != nil {
return nil, err
Expand Down
18 changes: 15 additions & 3 deletions controller/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,14 @@ func Test_mkRenewer(t *testing.T) {
podName string
commonName string
namespace string
renewalVolume string
}
tests := []struct {
name string
args args
want corev1.Container
}{
{"ok", args{&Config{CaURL: "caURL", ClusterDomain: "clusterDomain"}, "podName", "commonName", "namespace"}, corev1.Container{
{"mkRenewer-noVolume", args{&Config{CaURL: "caURL", ClusterDomain: "clusterDomain"}, "podName", "commonName", "namespace", ""}, corev1.Container{
Env: []corev1.EnvVar{
{Name: "STEP_CA_URL", Value: "caURL"},
{Name: "COMMON_NAME", Value: "commonName"},
Expand All @@ -98,10 +99,21 @@ func Test_mkRenewer(t *testing.T) {
{Name: "CLUSTER_DOMAIN", Value: "clusterDomain"},
},
}},
}
{"mkRenewer-Volume", args{&Config{CaURL: "caURL", ClusterDomain: "clusterDomain"}, "podName", "commonName", "namespace", "renewerVolume"}, 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"},
},
VolumeMounts: []corev1.VolumeMount{
{Name: "renewerVolume", MountPath: "/renewer", ReadOnly: true},
},
}}, }
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.renewalVolume); !reflect.DeepEqual(got, tt.want) {
t.Errorf("mkRenewer() = %v, want %v", got, tt.want)
}
})
Expand Down
4 changes: 3 additions & 1 deletion renewer/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@ ENV CRT="/var/run/autocert.step.sm/site.crt"
ENV KEY="/var/run/autocert.step.sm/site.key"
ENV STEP_ROOT="/var/run/autocert.step.sm/root.crt"

ENTRYPOINT ["/bin/bash", "-c", "step ca renew --daemon $CRT $KEY"]
COPY renewer/renewer.sh /home/step/
RUN chmod +x /home/step/renewer.sh
CMD ["/home/step/renewer.sh"]
10 changes: 10 additions & 0 deletions renewer/renewer.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#!/bin/sh -x

SCRIPT_PATH=/renewer/renew.sh

if [ -e "$SCRIPT_PATH" ]
then
CMD="--exec=$SCRIPT_PATH"
fi

/bin/bash -xc "step ca renew $CMD --daemon $CRT $KEY"