cert-rotation: New chart for certificate rotation

This chart creates a cronjob which monitors the expiry of the
certificates created by jetstack cert-manager. It rotates the
certificates and restarts the pods that mounts the certificate
secrets so that the new certificate can take effect.

Change-Id: I492b5f319cf0f2e7ccbbcf516953e17aafc1c59f
This commit is contained in:
Gupta, Sangeet (sg774j) 2021-04-26 18:18:41 +00:00 committed by Sangeet Gupta
parent 8351fdd0f1
commit f94aed3c7a
8 changed files with 562 additions and 0 deletions

20
cert-rotation/Chart.yaml Normal file
View File

@ -0,0 +1,20 @@
# 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.
---
apiVersion: v1
appVersion: "1.0"
description: Rotate the certificates generated by cert-manager
home: https://cert-manager.io/
name: cert-rotation
version: 0.1.0
...

View File

@ -0,0 +1,18 @@
# 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.
---
dependencies:
- name: helm-toolkit
repository: http://localhost:8879/charts
version: ">= 0.1.0"
...

View File

@ -0,0 +1,207 @@
#!/bin/bash
set -e
{{/*
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.
*/}}
COMMAND="${@:-rotate_job}"
namespace={{ .Release.Namespace }}
minDaysToExpiry={{ .Values.jobs.rotate.max_days_to_expiry }}
rotateBefore=$(($(date +%s) + (86400*$minDaysToExpiry)))
# Return Code, initialized to success
rc=0
function rotate_and_get_certs_list(){
# Rotate the certificates if the expiry date of certificates is within the
# max_days_to_expiry days
# List of secret and certificates rotated
local -n secRotated=$1
deleteAllSecrets=$2
certRotated=()
for certificate in $(kubectl get certificates -n ${namespace} --no-headers | awk '{ print $1 }')
do
certInfo=($(kubectl get certificate -n ${namespace} ${certificate} -o json | jq -r '.spec["secretName"],.status["notAfter"]'))
secretName=${certInfo[0]}
notAfter=$(date -d"${certInfo[1]}" '+%s')
deleteSecret=false
if ${deleteAllSecrets} || [ ${rotateBefore} -gt ${notAfter} ]
then
# Rotate the certificates/secrets and add to list.
echo "Deleting secret: ${secretName}"
kubectl delete secret -n ${namespace} $secretName
secRotated+=(${secretName})
certRotated+=(${certificate})
fi
done
# Ensure certificates are re-issued
if [ ! -z ${certRotated} ]
then
for cert in ${certRotated[@]}
do
counter=0
while [ "$(kubectl get certificate -n ${namespace} ${cert} -o json | jq -r '.status.conditions[].status')" != "True" ]
do
# Wait for secret to become ready. Wait for 300 seconds maximum. Sleep for 10 seconds
if [ ${counter} -ge 30 ]
then
echo "ERROR: Rotated certificate ${cert} in ${namespace} is not ready."
# Set return code to error and continue so that the certificates that are
# rotated successfully are deployed.
rc=1
break
fi
echo "Rotated certificate ${cert} in ${namespace} is not ready yet ... waiting"
counter+=(${counter+=1})
sleep 10
done
done
fi
}
function get_cert_list_rotated_by_cert_manager_rotate(){
local -n secRotated=$1
# Get the time when the last cron job was run successfully
lastCronTime=$(kubectl get jobs -n ${namespace} --no-headers -l application=cert-manager,component=cert-rotate -o json | jq -r '.items[] | select(.status.succeeded != null) | .status.completionTime' | sort -r | head -n 1)
if [ ! -z ${lastCronTime} ]
then
lastCronTimeSec=$(date -d"${lastCronTime}" '+%s')
for certificate in $(kubectl get certificates -n ${namespace} --no-headers | awk '{ print $1 }')
do
certInfo=($(kubectl get certificate -n ${namespace} ${certificate} -o json | jq -r '.spec["secretName"],.status["notBefore"]'))
secretName=${certInfo[0]}
notBefore=$(date -d"${certInfo[1]}" '+%s')
# if the certificate was created after last cronjob run means it was
# rotated by the cert-manager, add to the list.
if [[ ${notBefore} -gt ${lastCronTimeSec} ]]
then
secRotated+=(${secretName})
fi
done
fi
}
function restart_the_pods(){
local -n secRotated=$1
if [ -z ${secRotated} ]
then
echo "All certificates are still valid in ${namespace} namespace. No pod needs restart"
exit 0
fi
# Restart the pods using kubernetes rollout restart. This will restarts the applications
# with zero downtime.
for kind in statefulset deployment daemonset
do
# Need to find which kinds mounts the secret that has been rotated. To do this
# for a kind (statefulset, deployment, or daemonset)
# - get the name of the kind (which will index 1 = idx=0 of the output)
# - get the names of the secrets mounted on this kind (which will be index 2 = idx+1)
# - find if tls.crt was mounted to the container: get the subpaths of volumeMount in
# the container and grep for tls.crt. (This will be index 2 = idx+2)
resource=($(kubectl get ${kind} -n ${namespace} -o custom-columns='NAME:.metadata.name,SECRETS:.spec.template.spec.volumes[*].secret.secretName,TLS:.spec.template.spec.containers[*].volumeMounts[*].subPath' --no-headers | grep tls.crt))
idx=0
while [[ $idx -lt ${#resource[@]} ]]
do
# Name of the kind
resourceName=${resource[$idx]}
# List of secrets mounted to this kind
resourceSecrets=${resource[$idx+1]}
# For each secret mounted to this kind, check if it was rotated (present in
# the list secRotated) and if it was, then trigger rolling restart for this kind.
for secret in ${resourceSecrets//,/ }
do
if [[ "${secRotated[@]}" =~ "${secret}" ]]
then
echo "Restarting ${kind} ${resourceName} in ${namespace} namespace."
kubectl rollout restart -n ${namespace} ${kind} ${resourceName}
break
fi
done
# Since we have 3 custom colums in the output, every 4th index will be start of new tuple.
# Jump to the next tuple.
idx=$((idx+3))
done
done
}
function rotate_cron(){
# Rotate cronjob invoked this script.
# 1. If the expiry date of certificates is within the max_days_to_expiry days
# the rotate the certificates and restart the pods
# 2. Else if the certificates were rotated by cert-manager, then restart
# the pods.
secretsRotated=()
deleteAllSecrets=false
rotate_and_get_certs_list secretsRotated $deleteAllSecrets
if [ ! -z ${secretsRotated} ]
then
# Certs rotated, restart pods
restart_the_pods secretsRotated
else
# Check if the certificates were rotated by the cert-manager and get the list of
# rotated certificates so that the corresponding pods can be restarted
get_cert_list_rotated_by_cert_manager_rotate secretsRotated
if [ ! -z ${secretsRotated} ]
then
restart_the_pods secretsRotated
else
echo "All certificates are still valid in ${namespace} namespace"
fi
fi
}
function rotate_job(){
# Rotate job invoked this script.
# 1. Rotate all certificates by deleting the secrets and restart the pods
secretsRotated=()
deleteAllSecrets=true
rotate_and_get_certs_list secretsRotated $deleteAllSecrets
if [ ! -z ${secretsRotated} ]
then
# Certs rotated, restart pods
restart_the_pods secretsRotated
else
echo "All certificates are still valid in ${namespace} namespace"
fi
}
$COMMAND
exit ${rc}

View File

@ -0,0 +1,25 @@
{{/*
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.
*/}}
{{- if .Values.manifests.configmap_bin }}
{{- $envAll := . }}
---
apiVersion: v1
kind: ConfigMap
metadata:
name: cert-rotate-bin
data:
rotate-certs.sh: |
{{ tuple "bin/_rotate-certs.sh.tpl" . | include "helm-toolkit.utils.template" | indent 4 }}
{{ end }}

View File

@ -0,0 +1,120 @@
{{/*
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.
*/}}
{{- if .Values.manifests.cron_job_cert_rotate}}
{{- $envAll := . }}
{{- $serviceAccountName := "cert-rotate-cron" }}
{{ tuple $envAll "cert_rotate" $serviceAccountName | include "helm-toolkit.snippets.kubernetes_pod_rbac_serviceaccount" }}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: {{ $serviceAccountName }}
rules:
- apiGroups:
- cert-manager.io
resources:
- certificates
verbs:
- get
- list
- update
- patch
- apiGroups:
- "*"
resources:
- pods
- secrets
- jobs
- statefulsets
- daemonsets
- deployments
verbs:
- get
- list
- update
- patch
- delete
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: {{ $serviceAccountName }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: {{ $serviceAccountName }}
subjects:
- kind: ServiceAccount
name: {{ $serviceAccountName }}
namespace: {{ $envAll.Release.Namespace }}
---
apiVersion: batch/v1beta1
kind: CronJob
metadata:
name: cert-rotate
annotations:
{{ tuple $envAll | include "helm-toolkit.snippets.release_uuid" }}
labels:
{{ tuple $envAll "cert-manager" "cert-rotate-cron" | include "helm-toolkit.snippets.kubernetes_metadata_labels" | indent 4 }}
spec:
suspend: {{ .Values.jobs.rotate.suspend }}
schedule: {{ .Values.jobs.rotate.cron | quote }}
successfulJobsHistoryLimit: {{ .Values.jobs.rotate.history.success }}
failedJobsHistoryLimit: {{ .Values.jobs.rotate.history.failed }}
{{- if .Values.jobs.rotate.starting_deadline }}
startingDeadlineSeconds: {{ .Values.jobs.rotate.starting_deadline }}
{{- end }}
concurrencyPolicy: Forbid
jobTemplate:
metadata:
labels:
{{ tuple $envAll "cert-manager" "cert-rotate" | include "helm-toolkit.snippets.kubernetes_metadata_labels" | indent 8 }}
spec:
template:
metadata:
labels:
{{ tuple $envAll "cert-manager" "cert-rotate" | include "helm-toolkit.snippets.kubernetes_metadata_labels" | indent 12 }}
spec:
serviceAccountName: {{ $serviceAccountName }}
{{ dict "envAll" $envAll "application" "cert_rotate" | include "helm-toolkit.snippets.kubernetes_pod_security_context" | indent 10 }}
restartPolicy: OnFailure
nodeSelector:
{{ .Values.labels.job.node_selector_key }}: {{ .Values.labels.job.node_selector_value }}
initContainers:
{{ tuple $envAll "cert_rotate" list | include "helm-toolkit.snippets.kubernetes_entrypoint_init_container" | indent 12 }}
containers:
- name: cert-rotate
{{ tuple $envAll "cert_rotation" | include "helm-toolkit.snippets.image" | indent 14 }}
{{ tuple $envAll $envAll.Values.pod.resources.jobs.cert_rotate | include "helm-toolkit.snippets.kubernetes_resources" | indent 14 }}
{{ dict "envAll" $envAll "application" "cert_rotate" "container" "cert_rotate" | include "helm-toolkit.snippets.kubernetes_container_security_context" | indent 14 }}
command:
- /tmp/rotate-certs.sh
- rotate_cron
volumeMounts:
- name: pod-tmp
mountPath: /tmp
- name: cert-rotate-bin
mountPath: /tmp/rotate-certs.sh
subPath: rotate-certs.sh
readOnly: true
volumes:
- name: pod-tmp
emptyDir: {}
- name: cert-rotate-bin
configMap:
name: cert-rotate-bin
defaultMode: 0555
{{- end }}

View File

@ -0,0 +1,107 @@
{{/*
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.
*/}}
{{- if .Values.manifests.job_cert_rotate}}
{{- $envAll := . }}
{{- $serviceAccountName := "cert-rotate-job" }}
{{ tuple $envAll "cert_rotate" $serviceAccountName | include "helm-toolkit.snippets.kubernetes_pod_rbac_serviceaccount" }}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: {{ $serviceAccountName }}
rules:
- apiGroups:
- cert-manager.io
resources:
- certificates
verbs:
- get
- list
- update
- patch
- apiGroups:
- "*"
resources:
- pods
- secrets
- jobs
- statefulsets
- daemonsets
- deployments
verbs:
- get
- list
- update
- patch
- delete
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: {{ $serviceAccountName }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: {{ $serviceAccountName }}
subjects:
- kind: ServiceAccount
name: {{ $serviceAccountName }}
namespace: {{ $envAll.Release.Namespace }}
---
apiVersion: batch/v1
kind: Job
metadata:
name: cert-rotate-job
labels:
{{ tuple $envAll "cert-manager" "cert-rotate-job" | include "helm-toolkit.snippets.kubernetes_metadata_labels" | indent 4 }}
spec:
template:
metadata:
labels:
{{ tuple $envAll "cert-manager" "cert-rotate" | include "helm-toolkit.snippets.kubernetes_metadata_labels" | indent 8 }}
annotations:
{{ tuple $envAll | include "helm-toolkit.snippets.release_uuid" | indent 8 }}
spec:
serviceAccountName: {{ $serviceAccountName }}
{{ dict "envAll" $envAll "application" "cert_rotate" | include "helm-toolkit.snippets.kubernetes_pod_security_context" | indent 6 }}
restartPolicy: OnFailure
nodeSelector:
{{ .Values.labels.job.node_selector_key }}: {{ .Values.labels.job.node_selector_value }}
initContainers:
{{ tuple $envAll "cert_rotate" list | include "helm-toolkit.snippets.kubernetes_entrypoint_init_container" | indent 12 }}
containers:
- name: cert-rotate
{{ tuple $envAll "cert_rotation" | include "helm-toolkit.snippets.image" | indent 10 }}
{{ tuple $envAll $envAll.Values.pod.resources.jobs.cert_rotate | include "helm-toolkit.snippets.kubernetes_resources" | indent 10 }}
{{ dict "envAll" $envAll "application" "cert_rotate" "container" "cert_rotate" | include "helm-toolkit.snippets.kubernetes_container_security_context" | indent 10 }}
command:
- /tmp/rotate-certs.sh
- rotate_job
volumeMounts:
- name: pod-tmp
mountPath: /tmp
- name: cert-rotate-bin
mountPath: /tmp/rotate-certs.sh
subPath: rotate-certs.sh
readOnly: true
volumes:
- name: pod-tmp
emptyDir: {}
- name: cert-rotate-bin
configMap:
name: cert-rotate-bin
defaultMode: 0555
{{- end }}

61
cert-rotation/values.yaml Normal file
View File

@ -0,0 +1,61 @@
# 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.
---
images:
tags:
cert_rotation: 'docker.io/openstackhelm/ceph-config-helper:latest-ubuntu_bionic'
dep_check: 'quay.io/airshipit/kubernetes-entrypoint:v1.0.0'
local_registry:
active: false
labels:
job:
node_selector_key: openstack-control-plane
node_selector_value: enabled
jobs:
rotate:
# Run at 1:00AM on 1st of each month
cron: "0 1 1 * *"
starting_deadline: 600
history:
success: 3
failed: 1
# Number of day before expiry should certs be rotated.
max_days_to_expiry: 45
suspend: false
pod:
security_context:
cert_rotate:
pod:
runAsUser: 42424
container:
cert_rotate:
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
resources:
enabled: false
jobs:
cert_rotate:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "1024Mi"
cpu: "2000m"
dependencies:
static:
cert_rotate: null
manifests:
configmap_bin: true
cron_job_cert_rotate: false
job_cert_rotate: false
...

View File

@ -0,0 +1,4 @@
---
cert-rotation:
- 0.1.0 Initial Chart
...