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

Support multi-domain, ECC certificates, cluster mode #9

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion certs/templates/cronjob.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ spec:
spec:
template:
spec:
serviceAccountName: certs
serviceAccountName: {{ template "helper.fullname" . }}
containers:
- name: {{ template "helper.fullname" . }}
image: {{ template "helper.image" . }}
Expand Down
2 changes: 1 addition & 1 deletion certs/templates/job.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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" . }}
Expand Down
19 changes: 18 additions & 1 deletion certs/templates/role.yaml
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -8,4 +24,5 @@ rules:
verbs: ["get", "list", "update" , "create"]
- apiGroups: ["extensions"]
resources: ["ingresses"]
verbs: ["list"]
verbs: ["list"]
{{- end -}}
17 changes: 16 additions & 1 deletion certs/templates/rolebinding.yaml
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -8,4 +22,5 @@ subjects:
roleRef:
kind: Role
name: {{ template "helper.fullname" . }}
apiGroup: rbac.authorization.k8s.io
apiGroup: rbac.authorization.k8s.io
{{- end -}}
2 changes: 2 additions & 0 deletions certs/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ image:
repository: certs
tag: 1.0.1

cluster: false

schedule: "0 0,12 * * *"

backoffLimit: 1
Expand Down
175 changes: 116 additions & 59 deletions scripts/certs.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/bin/bash
#!/bin/sh
# Copyright 2019 Mathieu Naouache

set -e
Expand Down Expand Up @@ -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
Expand All @@ -94,77 +103,109 @@ 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}")
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deployment cluster scoped will failed. Please update your code to manage this case.
Use local keyword for variables when needed.

Copy link
Author

@fr0der1c fr0der1c Sep 25, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deployment cluster scoped will failed. Please update your code to manage this case.

What error did you see?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use local keyword for variables when needed.

I don't think sh supports local.

Copy link
Owner

@math-nao math-nao Sep 25, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What error did you see?

Cluster scoped deployment will failed due to a call requesting all namespaces without having a clusterrole. Just try it.

I don't think sh supports local.

ash is the system shell used on busybox and local keyword are supported.

Copy link
Author

@fr0der1c fr0der1c Sep 26, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cluster scoped deployment will failed due to a call requesting all namespaces without having a clusterrole. Just try it.

If you set cluster to true you will have a ClusterRole. If you set cluster to false(which is default), the request will fail. This is expected and will not cause problems. I've tested on my cluster. Maybe you need a fresh install? See certs/templates/rolebinding.yaml.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ash is the system shell used on busybox and local keyword are supported.

I'll look into this.

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}..."

for ingress in ${INGRESSES_FILTERED}; do
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"')
NAMESPACE=${namespace}

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 STATUS_CODE=$(k8s_api_call "GET" "/apis/extensions/v1beta1/namespaces/${NAMESPACE}/ingresses" 2>${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
if [ "${STATUS_CODE}" = "200" ]; then
format_res_file "${RES_FILE}"

if [ "${IS_DNS_VALID}" = "false" -a "${IS_CMD_TO_USE_VALID}" = "false" ]; then
return
local INGRESSES_FILTERED=$(cat "${RES_FILE}" | jq -c '.items | .[] | select(.metadata.annotations."acme.kubernetes.io/enable"=="true")')
rm -f "${RES_FILE}"

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() {
local NAME="$1"
shift
local DOMAINS="$@"

debug "Generate certs for" \
info "Generate certs for" \
" dns: ${CERTS_DNS}," \
" is_staging: ${CERTS_IS_STAGING}," \
" is_debug: ${CERTS_IS_DEBUG}," \
Expand All @@ -179,9 +220,10 @@ 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}'"
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"
Expand Down Expand Up @@ -210,13 +252,17 @@ generate_cert() {
if [ "${CERTS_CMD_TO_USE}" != "" ]; then
ACME_CMD="${CERTS_CMD_TO_USE}"
fi

MAIN_DOMAIN="$(echo "${DOMAINS}" | cut -d' ' -f1)"
info "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}"
Expand All @@ -231,10 +277,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
Expand Down Expand Up @@ -302,6 +350,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..."

Expand Down