Skip to content

Instantly share code, notes, and snippets.

@otheus
Last active November 17, 2022 23:37
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save otheus/3cc713c227dee30a4a44c11cad364e22 to your computer and use it in GitHub Desktop.
Save otheus/3cc713c227dee30a4a44c11cad364e22 to your computer and use it in GitHub Desktop.
## Provide Puppet Certificate of a given Node
##
## Otheus - 2022-07-21
##
## Notes to webmaster:
## * Modify this include file and stick it in `/etc/httpd/conf.d`. Alternatively, create a soft-link to
## this file in its repository-clone on that server (ensure directory permissions are kosher and
## readable by Apache).
##
## * Generate a secret and store that in a file indicated by `GET_PUPPET_NODE_SECRET`. The path
## should not be in the DocumentRoot nor CGI directory. The contents of the file should be something like:
## openssl rand -base64 12 | tr -d +=/
## This ensures a 12+ character alphanumeric secret.
## See the Dockerfile for more explicity help creating the file.
## Let the users know what the secret is. :)
##
## * Modify the location to the .cgi script and secret in the `Define` directives at the top (below comments).
##
## * If desired, modify the URI for this resource by changing the `Alias` and `Location` directives.
## Ensure _alias_ module is loaded in conf.modules.d.
##
## * Double-check the CGI configuration overall. Sometimes, CGI modules are disabled, but default configuration
## is overly permissive. Here, we load the CGI modules, in case they aren't. If the invocation of the URL
## shows the source code, the right modules might not be loading correctly.
##
##
Define GET_PUPPET_NODE_SECRET /var/www/uibk/.secret
Define GET_PUPPET_NODE_CGIDIR /var/www/uibk/cgi
<IfModule !mpm_prefork_module>
<IfModule !cgid_module>
LoadModule cgid_module modules/mod_cgid.so
</IfModule>
</IfModule>
<IfModule mpm_prefork_module>
<IfModule !cgi_module>
LoadModule cgi_module modules/mod_cgi.so
</IfModule>
</IfModule>
Alias /_uibk/get-puppet-node-cert ${GET_PUPPET_NODE_CGIDIR}/get-puppet-node-cert.cgi
<Location /_uibk/get-puppet-node-cert>
SetHandler cgi-script
Options +ExecCGI
SetEnv _AUTHTOKEN_FILE ${GET_PUPPET_NODE_SECRET}
<RequireAny>
Require ip 138.232.0.0/16
Require ip 172.24.0.0/22
Require host localhost
## _IF_DOCKER_ ## Require ip 172.17.0.0/16
</RequireAny>
</Location>
<Files ${GET_PUPPET_NODE_SECRET}>
Require all denied
</Files>
#!/bin/bash
# vim: sw=2 expandtab
#
# Given a nodename, produce the node's current public certifcate or key
# depending on query_string: t=cert or t=key
#
# CHANGELOG
# 2022-07-21 -- Initial version
# 2022-07-29 -- Use AUTHTOKEN_FILE for location of secret instead of UIBK....
# Validate auth not by reading file but using grep to compare.
_VERSION="2022-07-21"
_AUTHOR="Otheus.uibk@gmail.com"
## Global for configuration
: ${_AUTHTOKEN_FILE:="/dev/null"} # Configure here or specify via environment
: ${_CERT_DIRECTORY:="/etc/puppetlabs/puppet/ssl/ca/signed"}
: ${SCRIPT_NAME:=${BASH_SOURCE[0]}}
: ${_FACILITY:=local0}
## globals
_CERTPATH="/dev/null" # will be updated
set -e -o pipefail
function urldecode() { : "${*//+/ }"; echo -e "${_//%/\\x}"; }
## See https://stackoverflow.com/a/37840948/3849157
emit_header() {
echo "Content-type: $1"
echo ""
}
emit_status() {
echo "Status: ${1:-200} ${2:-OK}"
if [[ -n $3 ]]; then
echo "Content-type: text/plain"; echo;
echo "$2: $3"
fi
}
logit() {
local level="${1##*.}"
local facility="${1%.*}"
shift
if [[ "$level" = "$facility" ]]; then facility=${_FACILITY}; fi
if [[ -t 1 ]] ; then
echo >&2 "$level:" "$@"
else
command logger -s -p $facility.$level "$SCRIPT_NAME: " "$@"
fi
}
## fail_validation ALWAYS exits
## exit code is always 0, but we could change that for cli usage
fail_validation() {
logit err "$2: $3"
emit_status "${1:-400}" "${2:-Bad Request}" "$3"
exit 0
}
validate_key() {
## Only allow certain characters in the query-key/variable/parameter: alphanumeric and period, underscore
if ! [[ "$1" =~ ^[a-zA-Z0-9_.]*$ ]]; then
fail_validation 401 "Invalid input" "Invalid characters in key-variable $1"
fi
case "$1" in
auth|node|mode) ;;
*) fail_validation 401 "Invalid input" "Invalid variable $1" ;;
esac
}
validate_val() {
## Only allow characters valid in hostnames, base64 encodings. Not even spaces.
if ! [[ "$1" =~ ^[a-zA-Z0-9_.+/=-]*$ ]]; then
fail_validation 401 "Invalid input" "Invalid characters in right-hand-side of $2"
fi
}
validate_node() {
local _node="${1:-${_OPT[node]}}"
# regex taken from
if ! [[ "$_node" =~ ^(([a-zA-Z]|[a-zA-Z][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z]|[A-Za-z][A-Za-z0-9\-]*[A-Za-z0-9])$ ]]; then
fail_validation 401 "Invalid input" "Invalid input parameter node=$_node"
fi
}
validate_mode() {
local _mode="${1:-${_OPT[mode]}}"
case "$_mode" in
pem|cert|key|der|x509|p7b|pfx|pkcs12|pkcs7)
;; # noop
"")
# Note: it's perfectly acceptable for this variable to not exist or be defined
;;
*)
fail_validation 401 "Invalid input" "Invalid input parameter mode=$_mode"
;;
esac
}
validate_auth() {
## input: "$1" is authorization token from client
local _auth="${1:-${_OPT[auth]}}"
if [[ -z "$_auth" ]]; then
logit authpriv.warn "No auth token provided"
emit_status 403 "Invalid Authorization" "Talk to the $SERVER_NAME administrator!"
exit 0
elif ! [[ "$_auth" =~ ^[[:alnum:]]{12,}$ ]] ; then
logit authpriv.warn "Auth token has invalid characters or is too short"
emit_status 403 "Invalid Authorization" "Talk to the $SERVER_NAME administrator!"
exit 0
fi
## Assert: _auth variable is validated and free of dangerous characters
if [[ -z "${_AUTHTOKEN_FILE}" ]] || ! [[ -r "${_AUTHTOKEN_FILE}" ]] ; then
emit_status 500 "CGI Script failure" "Cannot find authentication token-file: ${_AUTHTOKEN_FILE}"
exit 0
elif ! awk 'NR > 1 || !/^[[:alnum:]]{12,}$/ { exit(1); }' "${_AUTHTOKEN_FILE}" ; then
logit err "Invalid auth token in $_AUTHTOKEN_FILE: must be one line of 12 or more alphanumeric characters"
emit_status 501 "CGI Script failure" "No valid authentication token on server to compare!"
exit 0
fi
## Assert: Authtoken file contains one line of 12+ valid password characters (alnum)
if ! grep -q -F -x -f - "${_AUTHTOKEN_FILE}" <<< "${_auth}"; then
logit authpriv.err "Wrong auth token provided from $REMOTE_ADDR, does not match found in ${_AUTHTOKEN_FILE}"
emit_status 403 "Invalid Authorization" "Talk to the $SERVER_NAME administrator!"
exit 0
fi
}
parse_querystring() {
### parse "$1" (QUERY_STRING if not provided)
## USE EXTREME CAUTION
## ENSURE NO "TAINTED" INPUT CAN BE USED AS ANOTHER INPUT
declare -g -A _OPT
local -a _keyval
local _kv _val _key _valdecoded
IFS='&' read -ra _keyval <<< "${1:-$QUERY_STRING}"
for _kv in "${_keyval[@]}"; do
IFS='=' read -r _key _val <<< "$_kv"
[[ "$_key" ]] || continue
validate_key "$_key"
_valdecoded=$(urldecode "$_val")
validate_val "$_valdecoded" "$_key"
_OPT[$_key]="$_valdecoded"
# echo >&2 "DEBUG: $_key => ${_OPT[$_key]}"
done
}
find_puppet_certificate_for_node() {
## SIDE-EFFECT : sets _CERTPATH to the filepath of the found certificate0
local _node="${1:-${_OPT[node]}}"
local _f _filepath _found
## Try the node name with various common domain names
for _f in "${_node}" "${_node}.YOUR_DOMAIN_1" "${_node}.YOUR_DOMAIN_2" ; do
_filepath="${_CERT_DIRECTORY}/${_f}.pem"
if [[ -r "$_filepath" ]]; then _found=1 ; break ; fi
done
if ! [[ $_found ]]; then
fail_validation 404 "Not found" "Cannot find or read certificate file for <${_node}>"
fi
_CERTPATH="$_filepath"
}
do_work() {
local _mode="${_OPT[mode]}"
case "${_mode:-key}" in
pem|x509|cert|"")
emit_header "application/x-pem-file"
cat "$_CERTPATH"
;;
der)
emit_header "application/octet-stream"
openssl x509 -noout -pubkey -in "$_CERTPATH" -outform der |openssl enc -base64 -d
;;
key) # default
emit_header "text/plain"
openssl x509 -pubkey -noout -in "$_CERTPATH"
;;
*)
fail_validation 401 "Invalid mode" "Invalid output mode: $_mode" ;;
esac
if [[ $? != 0 ]]; then
logit err "do_work program exited with non-zero. mode=${_mode} certpath=${_CERTPATH}"
else
logit info "delivered $_CERTPATH with mode=${_mode}"
fi
}
######################
if [[ ${BASH_VERSION%%.*} -lt 4 ]] ; then
fail_validation 501 "Not Implemented" "Requires BASH version >= 4.0 (associative arrays)"
fi
if [[ -z $SERVER_NAME ]]; then SERVER_NAME=`hostname -f`; fi
parse_querystring "$*"
validate_auth
validate_mode
validate_node
find_puppet_certificate_for_node
do_work
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment