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
Last active
January 2, 2024 20:08
-
-
Save dud225/6414c0d7bb27a44b3fe105a0f0f5a545 to your computer and use it in GitHub Desktop.
PKI with easy-rsa
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
- runs with Docker to improve compatibility and support accros OSes | |
- use git-crypt to encrypt your private keys |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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