Skip to content

Instantly share code, notes, and snippets.

@dud225
Last active January 2, 2024 20:08
Show Gist options
  • Save dud225/6414c0d7bb27a44b3fe105a0f0f5a545 to your computer and use it in GitHub Desktop.
Save dud225/6414c0d7bb27a44b3fe105a0f0f5a545 to your computer and use it in GitHub Desktop.
PKI with easy-rsa
- runs with Docker to improve compatibility and support accros OSes
- use git-crypt to encrypt your private keys
# syntax=docker/dockerfile:1
FROM debian:bullseye
SHELL ["/bin/bash", "-euxo", "pipefail", "-c"]
RUN <<EOT
apt-get update;
apt-get upgrade --assume-yes;
apt-get install --assume-yes --no-install-recommends easy-rsa expect vim;
EOT
ENV PATH="${PATH}:/usr/share/easy-rsa"
git config diff.openssl_crl.textconv "openssl crl -noout -text -in"
git config diff.openssl_x509.textconv "openssl x509 -noout -text -in"

cat >> .gitattributes << EOF
crl.pem diff=openssl_crl
*.crt diff=openssl_x509
EOF
#!/usr/bin/env bash
set -eu -o pipefail
IMG="easy-rsa"
WORKDIR="$(cd "$(dirname "${0}")"; pwd)"
ask_common_name() {
read -p "Enter the Common Name: " -r
echo
if [ -z "${REPLY}" ]; then
echo "Missing Common Name" >&2
exit 1
fi
common_name="${REPLY,,}"
}
build_docker_image() {
if ! docker inspect "${IMG}" &> /dev/null ||
[ -z "$(docker inspect "${IMG}" --format='{{ index .Config.Labels "org.opencontainers.artifact.created" }}')" ] ||
# don't rebuild the image if it's less than 1d old
[ "$(date -d "$(docker inspect "${IMG}" --format='{{ index .Config.Labels "org.opencontainers.artifact.created" }}')" +%s)" -le "$(date -d "1 day ago" +%s)" ]
then
docker buildx build --pull --tag "${IMG}" --load --label org.opencontainers.artifact.created="$(date --rfc-3339=seconds)" - < "${WORKDIR}/Dockerfile"
fi
}
run_container() {
# shellcheck disable=SC2054
local docker_opts=(
--name pki --hostname pki \
--rm --tty --interactive \
--user "$(id -u):$(id -g)" --mount type=bind,source=/etc/passwd,destination=/etc/passwd,readonly --mount type=bind,source=/etc/group,destination=/etc/group,readonly \
--mount type=bind,source="${WORKDIR}",destination="${WORKDIR}" --workdir "${WORKDIR}"
)
docker run "${docker_opts[@]}" "${IMG}" "${@}"
}
run_easyrsa() {
run_container easyrsa "${@}"
}
generate_cert() {
ask_common_name
build_docker_image
run_easyrsa --batch build-client-full "${common_name}" nopass
echo
echo "The x509 certificate has been stored in ${WORKDIR}/pki/issued/${common_name}.crt and the private key in ${WORKDIR}/pki/private/${common_name}.key"
}
export_pfx() {
ask_common_name
build_docker_image
local pkidir="${WORKDIR}/pki"
if ! { [ -f "${pkidir}/private/${common_name}.key" ] && [ -f "${pkidir}/issued/${common_name}.crt" ] ;}; then
echo "Missing key or certificate" >&2
exit 1
fi
local export_password
if [ -f "${pkidir}/private/${common_name}.pass.txt" ]; then
export_password="$(< "${pkidir}/private/${common_name}.pass.txt")"
else
export_password="$(openssl rand -base64 12)"
echo "${export_password}" > "${pkidir}/private/${common_name}.pass.txt"
fi
trap 'rm -f ${WORKDIR}/expect-script' EXIT
cat > "${WORKDIR}/expect-script" << EOF
spawn easyrsa --batch export-p12 ${common_name}
expect "Enter Export Password:"
send "${export_password}\r"
expect "Verifying - Enter Export Password:"
send "${export_password}\r"
expect eof
EOF
run_container expect -f expect-script
tar --directory="${pkidir}" --create --transform='s:private/::' --transform='s:issued/::' --file "${WORKDIR}/${common_name}.tar" \
"private/${common_name}.key" "private/${common_name}.pass.txt" "issued/${common_name}.crt" "private/${common_name}.p12"
echo
echo "The certificates have been stored in the archive ${WORKDIR}/${common_name}.tar"
}
list() {
cmd_output=$(mktemp)
trap 'rm ${cmd_output}' EXIT
if ! run_container easyrsa update-db > "${cmd_output}"; then
echo "Failed to update the CA database:" >&2
cat "${cmd_output}"
exit 1
fi
# shellcheck disable=SC2016
run_container awk -F '\t' 'BEGIN {
printf "%s\t%s\t%s\t%s\n", "certificate", "serial number", "status", "expiration"
} {
cert = $6
sn = $4
if ($1 == "V")
status = "valid"
else if ($1 == "R") {
date = substr($3, 1, index($3, ",")-1)
reason = substr($3, index($3, ",")+1)
gsub(/[0-9][0-9]/, "& ", date)
gsub(/Z$/, "", date)
revocation = strftime("%c", mktime(sprintf("20%s", date)))
status = sprintf("revoked since %s (reason: %s)", revocation, reason)
}
else if ($1 == "E")
status = "expired"
else
exit 1
gsub(/[0-9][0-9]/, "& ", $2)
gsub(/Z$/, "", $2)
expiration = strftime("%c", mktime(sprintf("20%s", $2)))
printf "%s\t%s\t%s\t%s\n", cert, sn, status, expiration
}' pki/index.txt |
# https://www.gnu.org/software/bash/manual/html_node/ANSI_002dC-Quoting.html
column -t -s $'\t'
}
case "${1:-}" in
generate-cert)
generate_cert
;;
export-pfx)
export_pfx
;;
interactive)
build_docker_image
run_container bash
;;
list)
list
;;
*)
echo "Usage:" >&2
echo "${0} { generate-cert | export-pfx | interactive | list }" >&2
exit 1
;;
esac
#!/usr/bin/env bash
set -eu -o pipefail
IMG="easy-rsa"
WORKDIR="$(cd "$(dirname "${0}")"; pwd)"
CERT_DEFAULT_VALIDITY=$((365*4))
build_docker_image() {
if ! docker inspect "${IMG}" &> /dev/null ||
[ -z "$(docker inspect "${IMG}" --format='{{ index .Config.Labels "org.opencontainers.artifact.created" }}')" ] ||
# don't rebuild the image if it's less than 1d old
[ "$(date -d "$(docker inspect "${IMG}" --format='{{ index .Config.Labels "org.opencontainers.artifact.created" }}')" +%s)" -le "$(date -d "1 day ago" +%s)" ]
then
if docker inspect "${IMG}" &> /dev/null; then
docker image rm "${IMG}"
fi
docker buildx build --pull --tag "${IMG}" --label org.opencontainers.artifact.created="$(date --rfc-3339=seconds)" --load - < ../Dockerfile
fi
}
run_container() {
local docker_opts=(
--name pki --hostname pki \
--rm --tty --interactive \
--user "$(id -u):$(id -g)" --mount type=bind,source=/etc/passwd,destination=/etc/passwd,readonly --mount type=bind,source=/etc/group,destination=/etc/group,readonly \
--mount type=bind,source="${WORKDIR}",destination="${WORKDIR}" --workdir "${WORKDIR}"
)
build_docker_image
# shellcheck disable=SC2086
docker run "${docker_opts[@]}" "${IMG}" "${@}"
}
run_easyrsa() {
run_container easyrsa "${@}"
}
gen_crl() {
run_easyrsa gen-crl
}
list() {
cmd_output=$(mktemp)
trap 'rm ${cmd_output}' EXIT
if ! run_container easyrsa update-db > "${cmd_output}"; then
echo "Failed to update the CA database:" >&2
cat "${cmd_output}"
exit 1
fi
run_container awk 'BEGIN {
printf "%s\t%s\t%s\t%s\n", "certificate", "serial number", "status", "expiration"
} {
sn = $3
cert = $5
if ($1 == "V")
status = "valid"
else if ($1 == "R") {
cert = $6
sn = $4
date = substr($3, 1, index($3, ",")-1)
reason = substr($3, index($3, ",")+1)
gsub(/[0-9][0-9]/, "& ", date)
gsub(/Z$/, "", date)
revocation = strftime("%c", mktime(sprintf("20%s", date)))
status = sprintf("revoked since %s (reason: %s)", revocation, reason)
}
else if ($1 == "E")
status = "expired"
else
exit 1
gsub(/[0-9][0-9]/, "& ", $2)
gsub(/Z$/, "", $2)
expiration = strftime("%c", mktime(sprintf("20%s", $2)))
printf "%s\t%s\t%s\t%s\n", cert, sn, status, expiration
}' pki/index.txt |
# https://www.gnu.org/software/bash/manual/html_node/ANSI_002dC-Quoting.html
column -t -s $'\t'
}
gen_cert() {
local validity="${1}"
local fqdns="${*:2:${#}}"
while [ -z "${fqdns}" ]; do
read -p "Enter the FQDN: " -r fqdns
echo
done
for fqdn in ${fqdns}; do
run_easyrsa --days="${validity}" build-server-full "${fqdn}" nopass
echo
echo "The x509 certificate has been stored in ${WORKDIR}/pki/issued/${fqdn}.crt"
echo
done
}
revoke() {
local reason="${1}"
local fqdns="${*:2:${#}}"
while [ -z "${fqdns}" ]; do
read -p "Enter the FQDN: " -r fqdns
echo
done
for fqdn in ${fqdns}; do
run_easyrsa revoke "${fqdn}" "${reason}"
done
}
usage() {
echo "Usage:" >&2
echo "${0} { easy-rsa [easyrsa arg] | gen-cert [--days <validity days>] <FQDN>... | gen-crl | list | revoke [--reason [reason] <FQDN>... | shell }" >&2
}
main() {
case "${1:-}" in
easy-rsa)
shift
run_easyrsa "${@}"
;;
gen-cert)
shift
local validity="${CERT_DEFAULT_VALIDITY}"
if [ ${#} -gt 0 ]; then
if [ "${1}" = "--days" ]; then
if ! [[ "${2}" =~ ^[[:digit:]]+$ ]]; then
usage
exit 1
fi
validity="${2}"
shift 2
elif [[ "${*}" =~ "--days" ]]; then
usage
exit 1
fi
fi
gen_cert "${validity}" "${@}"
;;
gen-crl)
gen_crl
;;
list)
list
;;
revoke)
shift
local reason="unspecified"
if [ "${1}" = "--reason" ]; then
reason="${2}"
shift 2
elif [[ "${*}" =~ "--reason" ]]; then
usage
exit 1
fi
revoke "${reason}" "${@}"
gen_crl
;;
shell)
run_container bash
;;
*)
usage
exit 1
;;
esac
}
main "${@}"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment