From 8e989b6156c36f0910d47703d19221442bf21c9c Mon Sep 17 00:00:00 2001 From: "zhanzeyu.frederic" Date: Sat, 7 Sep 2019 11:48:40 +0800 Subject: [PATCH 1/4] support multi domain and ecc certificates --- scripts/certs.sh | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/scripts/certs.sh b/scripts/certs.sh index d83cd1d..748d556 100644 --- a/scripts/certs.sh +++ b/scripts/certs.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/bin/sh # Copyright 2019 Mathieu Naouache set -e @@ -77,14 +77,23 @@ format_res_file() { } get_domain_root() { - local DOMAIN_FOUND=$(find /acme.sh/*.* -type d | head -1) + local DOMAIN="$1" + + # ecc certificates has a different directory name + DOMAIN_FOUND=$(find /acme.sh/"${DOMAIN}_ecc" -type d 2>/dev/null | head -1) + if [ "${DOMAIN_FOUND}" != "" ]; then + echo $(basename "${DOMAIN_FOUND}" 2>/dev/null) + fi + + local DOMAIN_FOUND=$(find /acme.sh/"${DOMAIN}" -type d 2>/dev/null | head -1) if [ "${DOMAIN_FOUND}" != "" ]; then echo $(basename "${DOMAIN_FOUND}" 2>/dev/null) fi } get_cert_hash() { - local DOMAIN_NAME_ROOT=$(get_domain_root) + local DOMAIN="$1" + local DOMAIN_NAME_ROOT=$(get_domain_root "${DOMAIN}") if [ "${DOMAIN_NAME_ROOT}" != "" ]; then echo $(md5sum "/acme.sh/${DOMAIN_NAME_ROOT}/fullchain.cer") fi @@ -107,7 +116,12 @@ starter() { return fi + IFS=$'\n' for ingress in ${INGRESSES_FILTERED}; do + unset IFS + IS_SECRET_CERTS_ALREADY_EXISTS="false" + IS_SECRET_CONF_ALREADY_EXISTS="false" + CERTS_DNS=$(echo "${ingress}" | jq -rc '.metadata.annotations."acme.kubernetes.io/dns"') CERTS_CMD_TO_USE=$(echo "${ingress}" | jq -rc '.metadata.annotations."acme.kubernetes.io/cmd-to-use"') @@ -210,13 +224,17 @@ generate_cert() { if [ "${CERTS_CMD_TO_USE}" != "" ]; then ACME_CMD="${CERTS_CMD_TO_USE}" fi + + MAIN_DOMAIN="$(echo "${DOMAINS}" | cut -d' ' -f1)" + echo "main domain: ${MAIN_DOMAIN}" # get the domain root used by acme - local DOMAIN_NAME_ROOT=$(get_domain_root) + local DOMAIN_NAME_ROOT=$(get_domain_root "${MAIN_DOMAIN}") debug "domain name root: ${DOMAIN_NAME_ROOT}" # get current cert hash - CURRENT_CERT_HASH=$(get_cert_hash) + CURRENT_CERT_HASH=$(get_cert_hash "${MAIN_DOMAIN}") + debug "hash (before): ${CURRENT_CERT_HASH}" # generate certs debug "Running cmd: ${ACME_CMD}" @@ -231,10 +249,12 @@ generate_cert() { fi # update domain name root after certs creation - DOMAIN_NAME_ROOT=$(get_domain_root) + DOMAIN_NAME_ROOT=$(get_domain_root "${MAIN_DOMAIN}") + debug "domain name root (after): ${DOMAIN_NAME_ROOT}" # get new cert hash - NEW_CERT_HASH=$(get_cert_hash) + NEW_CERT_HASH=$(get_cert_hash "${MAIN_DOMAIN}") + debug "hash (after): ${CURRENT_CERT_HASH}" # update secrets only if certs has been updated if [ "${CURRENT_CERT_HASH}" != "${NEW_CERT_HASH}" ]; then From c70248739bbdf5eb9e67a6067451da072790847c Mon Sep 17 00:00:00 2001 From: "zhanzeyu.frederic" Date: Sat, 7 Sep 2019 19:27:56 +0800 Subject: [PATCH 2/4] check if cert exists in secret --- scripts/certs.sh | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/scripts/certs.sh b/scripts/certs.sh index 748d556..32bb0f2 100644 --- a/scripts/certs.sh +++ b/scripts/certs.sh @@ -178,7 +178,7 @@ generate_cert() { shift local DOMAINS="$@" - debug "Generate certs for" \ + info "Generate certs for" \ " dns: ${CERTS_DNS}," \ " is_staging: ${CERTS_IS_STAGING}," \ " is_debug: ${CERTS_IS_DEBUG}," \ @@ -193,6 +193,7 @@ generate_cert() { # get previous conf if it exists load_conf_from_secret + check_cert_from_secret # prepare acme cmd args ACME_ARGS="--issue --ca-file '${ACME_CA_FILE}' --cert-file '${ACME_CERT_FILE}' --key-file '${ACME_KEY_FILE}'" @@ -226,7 +227,7 @@ generate_cert() { fi MAIN_DOMAIN="$(echo "${DOMAINS}" | cut -d' ' -f1)" - echo "main domain: ${MAIN_DOMAIN}" + info "main domain: ${MAIN_DOMAIN}" # get the domain root used by acme local DOMAIN_NAME_ROOT=$(get_domain_root "${MAIN_DOMAIN}") @@ -322,6 +323,15 @@ load_conf_from_secret() { rm -f "${RES_FILE}" } +check_cert_from_secret() { + info "Checking if cert in secret..." + + local STATUS_CODE=$(k8s_api_call "GET" /api/v1/namespaces/${NAMESPACE}/secrets/${CERTS_SECRET_NAME} 2>/dev/null) + if [ "${STATUS_CODE}" = "200" ]; then + IS_SECRET_CERTS_ALREADY_EXISTS="true" + fi +} + add_conf_to_secret() { info "Adding conf to secret..." From ceb6b1b0163c74f9646b3aba16a3fe1880509bcc Mon Sep 17 00:00:00 2001 From: "Miguel A. Alvarado V" Date: Fri, 30 Aug 2019 14:18:14 -0600 Subject: [PATCH 3/4] Fixing service account name in cronjob and job (cherry picked from commit 2a1632e541e26aa17fad0fbee7291816a46635f6) --- certs/templates/cronjob.yaml | 2 +- certs/templates/job.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/certs/templates/cronjob.yaml b/certs/templates/cronjob.yaml index 559fe8e..fd53a17 100644 --- a/certs/templates/cronjob.yaml +++ b/certs/templates/cronjob.yaml @@ -19,7 +19,7 @@ spec: spec: template: spec: - serviceAccountName: certs + serviceAccountName: {{ template "helper.fullname" . }} containers: - name: {{ template "helper.fullname" . }} image: {{ template "helper.image" . }} diff --git a/certs/templates/job.yaml b/certs/templates/job.yaml index ce088d5..cfb22bc 100644 --- a/certs/templates/job.yaml +++ b/certs/templates/job.yaml @@ -14,7 +14,7 @@ spec: app: {{ template "helper.name" . }} release: {{ .Release.Name }} spec: - serviceAccountName: certs + serviceAccountName: {{ template "helper.fullname" . }} containers: - name: {{ template "helper.fullname" . }} image: {{ template "helper.image" . }} From 020dcc9771fe6de78bc83922f1ef2c283649bf7b Mon Sep 17 00:00:00 2001 From: "zhanzeyu.frederic" Date: Sun, 8 Sep 2019 02:20:17 +0800 Subject: [PATCH 4/4] support cluster wide & use fullchain cert --- README.md | 1 + certs/templates/role.yaml | 19 ++++- certs/templates/rolebinding.yaml | 17 +++- certs/values.yaml | 2 + scripts/certs.sh | 137 ++++++++++++++++++------------- 5 files changed, 119 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index e4a854b..f5d23e6 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ Parameter | Default | Description image.registry | `mathnao` | Set the docker image registry to use. image.repository | `certs` | Set the docker image repository to use. image.tag | `tag` | Set the docker image tag to use. +cluster | `false` | Cluster mode allows to serve all namespaces. schedule | `0 0,12 * * *` | Set the job schedule to run dns validation for certificate renew. backoffLimit | `1` | Specify the number of retries before considering a job as failed. activeDeadlineSeconds | `600` | Set an active deadline for terminatting a job. diff --git a/certs/templates/role.yaml b/certs/templates/role.yaml index a84506d..c585c2e 100644 --- a/certs/templates/role.yaml +++ b/certs/templates/role.yaml @@ -1,3 +1,19 @@ +{{- if .Values.cluster -}} +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ template "helper.fullname" . }} +rules: +- apiGroups: [""] + resources: ["secrets"] + verbs: ["get", "list", "update" , "create"] +- apiGroups: ["extensions"] + resources: ["ingresses"] + verbs: ["list"] +- apiGroups: [""] + resources: ["namespaces"] + verbs: ["get", "list"] +{{- else -}} apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: @@ -8,4 +24,5 @@ rules: verbs: ["get", "list", "update" , "create"] - apiGroups: ["extensions"] resources: ["ingresses"] - verbs: ["list"] \ No newline at end of file + verbs: ["list"] +{{- end -}} \ No newline at end of file diff --git a/certs/templates/rolebinding.yaml b/certs/templates/rolebinding.yaml index 8afa2ab..f84be6b 100644 --- a/certs/templates/rolebinding.yaml +++ b/certs/templates/rolebinding.yaml @@ -1,3 +1,17 @@ +{{- if .Values.cluster -}} +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ template "helper.fullname" . }} +subjects: +- kind: ServiceAccount + namespace: {{.Release.Namespace}} + name: {{ template "helper.fullname" . }} +roleRef: + kind: ClusterRole + name: {{ template "helper.fullname" . }} + apiGroup: rbac.authorization.k8s.io +{{- else -}} apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: @@ -8,4 +22,5 @@ subjects: roleRef: kind: Role name: {{ template "helper.fullname" . }} - apiGroup: rbac.authorization.k8s.io \ No newline at end of file + apiGroup: rbac.authorization.k8s.io +{{- end -}} \ No newline at end of file diff --git a/certs/values.yaml b/certs/values.yaml index 38c84a7..bd38d00 100644 --- a/certs/values.yaml +++ b/certs/values.yaml @@ -10,6 +10,8 @@ image: repository: certs tag: 1.0.1 +cluster: false + schedule: "0 0,12 * * *" backoffLimit: 1 diff --git a/scripts/certs.sh b/scripts/certs.sh index 32bb0f2..53483df 100644 --- a/scripts/certs.sh +++ b/scripts/certs.sh @@ -103,74 +103,101 @@ starter() { info "Initialize environment..." local RES_FILE=$(mktemp /tmp/init_env.XXXX) - local STATUS_CODE=$(k8s_api_call "GET" "/apis/extensions/v1beta1/namespaces/${NAMESPACE}/ingresses" 2>${RES_FILE}) - if [ "${STATUS_CODE}" = "200" ]; then + # try get namespaces (cluster mode) + status_code=$(k8s_api_call "GET" /api/v1/namespaces 2>"${RES_FILE}") + if [ "${status_code}" = "200" ]; then + info "Success getting all namesapces" format_res_file "${RES_FILE}" - - local INGRESSES_FILTERED=$(cat "${RES_FILE}" | jq -c '.items | .[] | select(.metadata.annotations."acme.kubernetes.io/enable"=="true")') - rm -f "${RES_FILE}" + namespaces=$(jq -c '.items | .[]' < "${RES_FILE}") + else + info "Invalid status code when getting namespaces: ${status_code}" + namespaces=NAMESPACE + fi - if [ "${INGRESSES_FILTERED}" = "" ]; then - info "No matching ingress found" - return - fi + # iterate over all namespaces + for namespace in ${namespaces}; do + namespace=$(echo "${namespace}" | jq -c -r '.metadata.name') + echo "Processing namespace ${namespace}..." - IFS=$'\n' - for ingress in ${INGRESSES_FILTERED}; do - unset IFS - IS_SECRET_CERTS_ALREADY_EXISTS="false" - IS_SECRET_CONF_ALREADY_EXISTS="false" + NAMESPACE=${namespace} - CERTS_DNS=$(echo "${ingress}" | jq -rc '.metadata.annotations."acme.kubernetes.io/dns"') - CERTS_CMD_TO_USE=$(echo "${ingress}" | jq -rc '.metadata.annotations."acme.kubernetes.io/cmd-to-use"') + local STATUS_CODE=$(k8s_api_call "GET" "/apis/extensions/v1beta1/namespaces/${NAMESPACE}/ingresses" 2>${RES_FILE}) - local IS_DNS_VALID="true" - if [ "${CERTS_DNS}" = "null" -o "${CERTS_DNS}" = "" ]; then - info "No dns configuration found" - IS_DNS_VALID="false" - # convert null to empty string - CERTS_DNS="" - fi + if [ "${STATUS_CODE}" = "200" ]; then + format_res_file "${RES_FILE}" - local IS_CMD_TO_USE_VALID="true" - if [ "${CERTS_CMD_TO_USE}" = "null" -o "${CERTS_CMD_TO_USE}" = "" ]; then - info "No cmd to use found" - IS_CMD_TO_USE_VALID="false" - # convert null to empty string - CERTS_CMD_TO_USE="" - fi + local INGRESSES_FILTERED=$(cat "${RES_FILE}" | jq -c '.items | .[] | select(.metadata.annotations."acme.kubernetes.io/enable"=="true")') + rm -f "${RES_FILE}" - if [ "${IS_DNS_VALID}" = "false" -a "${IS_CMD_TO_USE_VALID}" = "false" ]; then - return + if [ "${INGRESSES_FILTERED}" = "" ]; then + info "No matching ingress found in namespace ${NAMESPACE}" fi - CERTS_ARGS=$(echo "${ingress}" | jq -rc '.metadata.annotations."acme.kubernetes.io/add-args"') - if [ "${CERTS_ARGS}" = "null" -o "${CERTS_ARGS}" = "" ]; then - info "No cmd args found" - # convert null to empty string + IFS=$'\n' + for ingress in ${INGRESSES_FILTERED}; do + unset IFS + IS_SECRET_CERTS_ALREADY_EXISTS="false" + IS_SECRET_CONF_ALREADY_EXISTS="false" + CERTS_DNS="" + CERTS_IS_STAGING="false" + CERTS_IS_DEBUG="false" CERTS_ARGS="" - fi + CERTS_CMD_TO_USE="" + + CERTS_DNS=$(echo "${ingress}" | jq -rc '.metadata.annotations."acme.kubernetes.io/dns"') + CERTS_CMD_TO_USE=$(echo "${ingress}" | jq -rc '.metadata.annotations."acme.kubernetes.io/cmd-to-use"') + + local IS_DNS_VALID="true" + if [ "${CERTS_DNS}" = "null" -o "${CERTS_DNS}" = "" ]; then + info "No dns configuration found" + IS_DNS_VALID="false" + # convert null to empty string + CERTS_DNS="" + fi + + local IS_CMD_TO_USE_VALID="true" + if [ "${CERTS_CMD_TO_USE}" = "null" -o "${CERTS_CMD_TO_USE}" = "" ]; then + info "No cmd to use found" + IS_CMD_TO_USE_VALID="false" + # convert null to empty string + CERTS_CMD_TO_USE="" + fi + + if [ "${IS_DNS_VALID}" = "false" -a "${IS_CMD_TO_USE_VALID}" = "false" ]; then + return + fi + + CERTS_ARGS=$(echo "${ingress}" | jq -rc '.metadata.annotations."acme.kubernetes.io/add-args"') + if [ "${CERTS_ARGS}" = "null" -o "${CERTS_ARGS}" = "" ]; then + info "No cmd args found" + # convert null to empty string + CERTS_ARGS="" + fi + + if [ "$(echo "${ingress}" | jq -c '. | select(.metadata.annotations."acme.kubernetes.io/staging"=="true")' | wc -l)" = "1" ]; then + CERTS_IS_STAGING="true" + fi + + if [ "$(echo "${ingress}" | jq -c '. | select(.metadata.annotations."acme.kubernetes.io/debug"=="true")' | wc -l)" = "1" ]; then + CERTS_IS_DEBUG="true" + fi + + TLS_INPUTS=$(echo "${ingress}" | jq -c '.spec.tls | .[]') + for input in ${TLS_INPUTS}; do + local SECRETNAME=$(echo ${input} | jq -rc '.secretName') + local HOSTS=$(echo ${input} | jq -rc '.hosts | .[]' | tr '\n' ' ') + # no quotes on the last argument please + generate_cert "${SECRETNAME}" ${HOSTS} + done + done + else + info "Invalid status code found: ${STATUS_CODE}" + fi + done - if [ "$(echo "${ingress}" | jq -c '. | select(.metadata.annotations."acme.kubernetes.io/staging"=="true")' | wc -l)" = "1" ]; then - CERTS_IS_STAGING="true" - fi - if [ "$(echo "${ingress}" | jq -c '. | select(.metadata.annotations."acme.kubernetes.io/debug"=="true")' | wc -l)" = "1" ]; then - CERTS_IS_DEBUG="true" - fi - TLS_INPUTS=$(echo "${ingress}" | jq -c '.spec.tls | .[]') - for input in ${TLS_INPUTS}; do - local SECRETNAME=$(echo ${input} | jq -rc '.secretName') - local HOSTS=$(echo ${input} | jq -rc '.hosts | .[]' | tr '\n' ' ') - # no quotes on the last argument please - generate_cert "${SECRETNAME}" ${HOSTS} - done - done - else - info "Invalid status code found: ${STATUS_CODE}" - fi } generate_cert() { @@ -196,7 +223,7 @@ generate_cert() { check_cert_from_secret # prepare acme cmd args - ACME_ARGS="--issue --ca-file '${ACME_CA_FILE}' --cert-file '${ACME_CERT_FILE}' --key-file '${ACME_KEY_FILE}'" + ACME_ARGS="--issue --ca-file '${ACME_CA_FILE}' --fullchain-file '${ACME_CERT_FILE}' --key-file '${ACME_KEY_FILE}'" if [ "${CERTS_IS_DEBUG}" = "true" ]; then ACME_ARGS="${ACME_ARGS} --debug"