Last active
November 17, 2022 23:37
-
-
Save otheus/3cc713c227dee30a4a44c11cad364e22 to your computer and use it in GitHub Desktop.
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
## 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> |
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
#!/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