Skip to content

Commit a81b929

Browse files
authored
Merge pull request #1 from coopTilleuls/feature/cronjob-image
Cronjob cert secrets garbage collector
2 parents f01e9e7 + f930bed commit a81b929

File tree

6 files changed

+171
-0
lines changed

6 files changed

+171
-0
lines changed

.github/workflows/build.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,19 @@ on:
99
required: true
1010

1111
jobs:
12+
build-cronjob-cert-secrets-gc:
13+
runs-on: ubuntu-latest
14+
15+
steps:
16+
- name: Build-cronjob-cert
17+
uses: coopTilleuls/action-docker-build-push@v12
18+
with:
19+
IMAGE_NAME: cronjob-cert-secrets-gc
20+
BUILD_CONTEXT: ./cronjob/cert
21+
BUILD_TARGET: cronjob
22+
REGISTRY_JSON_KEY: ${{ secrets.GITHUB_TOKEN }}
23+
IMAGE_REPOSITORY: ghcr.io/cooptilleuls/sre/minimal
24+
1225
build-kubectl:
1326
runs-on: ubuntu-latest
1427

.github/workflows/cd.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
name: CD
22
on:
3+
pull_request:
4+
types: [ opened, synchronize, reopened, labeled ]
35
push:
46
branches:
57
- main

cronjob/cert/Dockerfile

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
FROM debian:bookworm-slim AS build
2+
RUN apt-get update -y && apt-get upgrade -y && apt-get install curl -y
3+
RUN curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" && \
4+
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl.sha256" && \
5+
echo "$(cat kubectl.sha256) kubectl" | sha256sum --check && \
6+
chmod +x kubectl && mv kubectl /usr/bin/
7+
8+
FROM debian:bookworm-slim AS cronjob
9+
COPY --from=build /usr/bin/kubectl /usr/bin/kubectl
10+
COPY --from=ghcr.io/jqlang/jq:latest /jq /usr/bin/jq
11+
COPY --chmod=0755 cert-secrets-gc.sh /usr/local/bin/cert-secrets-gc.sh
12+
ENV PATH="/usr/local/bin:${PATH}"
13+
ENTRYPOINT [ "cert-secrets-gc.sh" ]
14+
CMD [ "--dry" ]
15+
16+
# Fat image includes gcloud
17+
FROM google/cloud-sdk:latest AS cloud-sdk-base
18+
RUN apt-get update && apt-get install -y google-cloud-sdk-gke-gcloud-auth-plugin
19+
20+
FROM cronjob AS cronjob-gcloud
21+
RUN apt-get update -y && apt-get upgrade -y && apt-get install python3 -y
22+
COPY --from=cloud-sdk-base /usr/lib/google-cloud-sdk /usr/lib/google-cloud-sdk
23+
ENV CLOUDSDK_ROOT_DIR="/usr/lib/google-cloud-sdk"
24+
ENV PATH="${CLOUDSDK_ROOT_DIR}/bin:${PATH}"

cronjob/cert/Event.template.json

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"apiVersion": "v1",
3+
"kind": "Event",
4+
"metadata": {
5+
"name": "{{event-name}}",
6+
"namespace": "kube-system"
7+
},
8+
"involvedObject": {
9+
"apiVersion": "v1",
10+
"kind": "Secret",
11+
"name": "{{secret-name}}",
12+
"namespace": "{{secret-namespace}}"
13+
},
14+
"reportingComponent": "cronjob-cert-secrets-gc",
15+
"reportingInstance": "{{host-name}}",
16+
"source": {
17+
"component": "cronjob-cert-secrets-gc",
18+
"host": "{{host-name}}"
19+
},
20+
"firstTimestamp": "{{first-timestamp}}",
21+
"lastTimestamp": "{{last-timestamp}}",
22+
"message": "{{message}}",
23+
"reason": "UnusedSecret",
24+
"type": "{{warning-level}}"
25+
}

cronjob/cert/README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Cert Secrets Garbage Collector
2+
3+
This image deletes unused secrets from the cluster
4+
5+
## Run the image in a cluster
6+
7+
This image needs access secrets and ingresses. So here we use service account `my-account` in namespace `my-namespace` which has the needed rights
8+
9+
```
10+
kubectl run -n my-namespace test-cert-secrets-gc --rm -i --image=ghcr.io/cooptilleuls/sre/minimal/cronjob-cert-secrets-gc:latest --overrides='{ "spec": { "serviceAccountName": "my-account" } }'
11+
```
12+
13+
## Run the image from local environment (outside the cluster)
14+
15+
### Build the image which includes gcloud sdk
16+
17+
The minimal image on the registry can not authenticate on google cloud. If you want to use it, you need to build the fat image which includes gcloud sdk
18+
19+
```
20+
docker build --network host -t cert-secrets-gc-fat .
21+
```
22+
23+
### Run the image with gcloud config
24+
25+
You can now run the image. Be sure to mount any relevant config as a volume in the container
26+
27+
```
28+
docker run --network host -v ~/.kube:/root/.kube -v ~/.config/gcloud:/root/.config/gcloud -it --rm cert-secrets-gc-fat
29+
```

cronjob/cert/cert-secrets-gc.sh

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
#!/bin/bash
2+
set -e
3+
set -x
4+
5+
if [[ "$1" == "--help" ]]; then
6+
echo "Usage: cert-secrets-gc.sh [option]"
7+
echo "Option:"
8+
echo " --help Show this help message"
9+
echo " --dry Dry run. Dont delete secrets"
10+
echo " --filter <regex> A regex expression used to filter secrets"
11+
exit 0
12+
fi
13+
14+
if [[ "$1" == "--filter" ]]; then
15+
SECRET_ANNOTATION_FILTER=$2
16+
fi
17+
18+
if [[ "$1" == "--dry" ]]; then
19+
echo "Argument --dry is set. Will NOT be deleting anything"
20+
fi
21+
22+
log_event() {
23+
local message="$1"
24+
local level="$2"
25+
local now=$(date -u --date='now' +%Y-%m-%dT%H:%M:%SZ)
26+
local secret_name="${3:-"default"}"
27+
local secret_namespace="${4:-"default"}"
28+
local event_name="cert-secrets-gc-$(date -u +%s%N)"
29+
local host_name=$(hostname -s)
30+
local template=$(cat Event.template.json)
31+
template=${template//\{\{message\}\}/$message}
32+
template=${template//\{\{warning-level\}\}/$level}
33+
template=${template//\{\{first-timestamp\}\}/$JOBSTART}
34+
template=${template//\{\{last-timestamp\}\}/$now}
35+
template=${template//\{\{secret-name\}\}/$secret_name}
36+
template=${template//\{\{secret-namespace\}\}/$secret_namespace}
37+
template=${template//\{\{event-name\}\}/$event_name}
38+
template=${template//\{\{host-name\}\}/$host_name}
39+
echo "$template" | kubectl create -f -
40+
}
41+
42+
# cert should have been renewed before this date
43+
# if it's not we delete the secret
44+
RENEWAL_DATE=$(date -d "@$(( $(date +%s) - 14 * 24 * 60 * 60 ))" +%s)
45+
46+
# secrets referenced in certificates
47+
kubectl get certificates -A -o json | jq -r ".items[] | ( .metadata.namespace + \"/\" + .spec.secretName )" >/tmp/secrets_from_certs
48+
49+
# secrets managed by cert manager and NOT referenced in certs
50+
kubectl get secrets -A -o json | jq -r '.items[] | select( .metadata.annotations["cert-manager.io/issuer-group"] == "cert-manager.io" and .type == "kubernetes.io/tls" ) | (.metadata.namespace + "/" + .metadata.name )' | grep -x -v -f /tmp/secrets_from_certs >/tmp/abandoned || [ $? -eq 1 ]
51+
52+
# secrets currently used by ingress
53+
kubectl get ing -A -o go-template='{{range .items}}{{$namespace := .metadata.namespace}}{{range .spec.tls }}{{$namespace}}/{{ .secretName }}{{ end }}{{printf "\n"}}{{ end}}' | sed -e '/^$/d; /<no value>/d' > /tmp/used
54+
55+
# secrets from certificates not renewed
56+
# we ignore certificates without renewalTime
57+
kubectl get certificates -A -o json | jq -r ".items[] | select( .status.renewalTime | select(type == \"string\") | fromdateiso8601 < $RENEWAL_DATE ) | ( .metadata.namespace + \"/\" + .spec.secretName )" >/tmp/not_renewed
58+
59+
# get list of unused secrets
60+
grep -v -x -f /tmp/used /tmp/not_renewed > /tmp/unused || echo "secrets not renewed not found"
61+
grep -v -x -f /tmp/used /tmp/abandoned >> /tmp/unused || echo "secrets without certs not found"
62+
# if there's a filter we use it restrictively
63+
# if it returns nothing, we don't delete secrets
64+
if [ -n "$SECRET_ANNOTATION_FILTER" ]; then cat /tmp/unused | grep "$SECRET_ANNOTATION_FILTER" >/tmp/unused_filtered ; mv /tmp/unused_filtered /tmp/unused; fi
65+
66+
for CERT in $(cat /tmp/unused ) ; do
67+
NAMESPACE=$${CERT%%/*}
68+
SECRET=$${CERT##*/}
69+
if kubectl -n $${NAMESPACE} get secret/$${SECRET} &>/dev/null ; then
70+
if [[ "$1" == "--dry" ]]; then
71+
echo "Not deleting $${NAMESPACE}}/$${SECRET}"
72+
else
73+
echo "Deleting $${NAMESPACE}}/$${SECRET}"
74+
log_event "Deleting secret $${NAMESPACE}}/$${SECRET}" "Warning" $${SECRET} $${NAMESPACE}
75+
kubectl -n $${NAMESPACE} delete secret/$${SECRET}
76+
fi
77+
fi
78+
done

0 commit comments

Comments
 (0)