All checks were successful
Co-authored-by: Jeffrey Smith <jasafpro@gmail.com> Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
238 lines
6.5 KiB
Bash
Executable File
238 lines
6.5 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# armature-ca.sh — Certificate provisioning for Armature mTLS.
|
|
#
|
|
# Wraps openssl to generate a cluster CA, node certificates (ServerAuth +
|
|
# ClientAuth), and user certificates (ClientAuth only). All output is PEM.
|
|
#
|
|
# Usage:
|
|
# armature-ca init
|
|
# armature-ca issue-node --name <node-name> --san <dns1,ip1,...>
|
|
# armature-ca issue-user --cn <username> [--email <email>]
|
|
#
|
|
# Files are written to the current directory under ca/, nodes/, users/.
|
|
|
|
set -euo pipefail
|
|
|
|
CA_DIR="./ca"
|
|
NODES_DIR="./nodes"
|
|
USERS_DIR="./users"
|
|
CA_DAYS=3650 # 10 years for CA
|
|
NODE_DAYS=365 # 1 year for nodes
|
|
USER_DAYS=90 # 90 days for users
|
|
|
|
# ── Helpers ──────────────────────────────────────────────────────────
|
|
|
|
die() { echo "error: $*" >&2; exit 1; }
|
|
|
|
require_openssl() {
|
|
command -v openssl >/dev/null 2>&1 || die "openssl not found in PATH"
|
|
}
|
|
|
|
require_ca() {
|
|
[ -f "$CA_DIR/cluster-ca.key" ] || die "CA not initialized. Run: $0 init"
|
|
[ -f "$CA_DIR/cluster-ca.crt" ] || die "CA not initialized. Run: $0 init"
|
|
}
|
|
|
|
# ── init ─────────────────────────────────────────────────────────────
|
|
|
|
cmd_init() {
|
|
require_openssl
|
|
mkdir -p "$CA_DIR"
|
|
|
|
if [ -f "$CA_DIR/cluster-ca.key" ]; then
|
|
die "CA already exists at $CA_DIR/cluster-ca.key — remove it first to reinitialize"
|
|
fi
|
|
|
|
# Generate ECDSA P-256 CA key
|
|
openssl ecparam -genkey -name prime256v1 -noout -out "$CA_DIR/cluster-ca.key" 2>/dev/null
|
|
|
|
# Self-signed CA certificate
|
|
openssl req -new -x509 \
|
|
-key "$CA_DIR/cluster-ca.key" \
|
|
-out "$CA_DIR/cluster-ca.crt" \
|
|
-days "$CA_DAYS" \
|
|
-subj "/CN=Armature Cluster CA" \
|
|
-addext "basicConstraints=critical,CA:TRUE" \
|
|
-addext "keyUsage=critical,keyCertSign,cRLSign" \
|
|
2>/dev/null
|
|
|
|
echo "CA initialized:"
|
|
echo " $CA_DIR/cluster-ca.crt"
|
|
echo " $CA_DIR/cluster-ca.key"
|
|
echo ""
|
|
echo "Keep cluster-ca.key secure. It never needs to be on a running node."
|
|
}
|
|
|
|
# ── issue-node ───────────────────────────────────────────────────────
|
|
|
|
cmd_issue_node() {
|
|
require_openssl
|
|
require_ca
|
|
|
|
local name=""
|
|
local san=""
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--name) name="$2"; shift 2 ;;
|
|
--san) san="$2"; shift 2 ;;
|
|
*) die "unknown option: $1" ;;
|
|
esac
|
|
done
|
|
|
|
[ -n "$name" ] || die "usage: $0 issue-node --name <node-name> --san <dns1,ip1,...>"
|
|
[ -n "$san" ] || die "usage: $0 issue-node --name <node-name> --san <dns1,ip1,...>"
|
|
|
|
mkdir -p "$NODES_DIR"
|
|
|
|
# Generate node key
|
|
openssl ecparam -genkey -name prime256v1 -noout -out "$NODES_DIR/$name.key" 2>/dev/null
|
|
|
|
# Build SAN entries from comma-separated list
|
|
local san_entries=""
|
|
IFS=',' read -ra ADDRS <<< "$san"
|
|
for addr in "${ADDRS[@]}"; do
|
|
addr=$(echo "$addr" | xargs) # trim whitespace
|
|
if [[ "$addr" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
|
san_entries="${san_entries}IP:${addr},"
|
|
else
|
|
san_entries="${san_entries}DNS:${addr},"
|
|
fi
|
|
done
|
|
san_entries="${san_entries%,}" # remove trailing comma
|
|
|
|
# Create CSR
|
|
openssl req -new \
|
|
-key "$NODES_DIR/$name.key" \
|
|
-out "$NODES_DIR/$name.csr" \
|
|
-subj "/CN=$name" \
|
|
2>/dev/null
|
|
|
|
# Create extensions config
|
|
local ext_file
|
|
ext_file=$(mktemp)
|
|
cat > "$ext_file" <<EOF
|
|
basicConstraints = CA:FALSE
|
|
keyUsage = critical, digitalSignature
|
|
extendedKeyUsage = serverAuth, clientAuth
|
|
subjectAltName = ${san_entries}
|
|
EOF
|
|
|
|
# Sign with CA
|
|
openssl x509 -req \
|
|
-in "$NODES_DIR/$name.csr" \
|
|
-CA "$CA_DIR/cluster-ca.crt" \
|
|
-CAkey "$CA_DIR/cluster-ca.key" \
|
|
-CAcreateserial \
|
|
-out "$NODES_DIR/$name.crt" \
|
|
-days "$NODE_DAYS" \
|
|
-extfile "$ext_file" \
|
|
2>/dev/null
|
|
|
|
rm -f "$NODES_DIR/$name.csr" "$ext_file"
|
|
|
|
echo "Node cert issued ($NODE_DAYS days):"
|
|
echo " $NODES_DIR/$name.crt"
|
|
echo " $NODES_DIR/$name.key"
|
|
echo ""
|
|
echo "Deploy to the node along with $CA_DIR/cluster-ca.crt"
|
|
}
|
|
|
|
# ── issue-user ───────────────────────────────────────────────────────
|
|
|
|
cmd_issue_user() {
|
|
require_openssl
|
|
require_ca
|
|
|
|
local cn=""
|
|
local email=""
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--cn) cn="$2"; shift 2 ;;
|
|
--email) email="$2"; shift 2 ;;
|
|
*) die "unknown option: $1" ;;
|
|
esac
|
|
done
|
|
|
|
[ -n "$cn" ] || die "usage: $0 issue-user --cn <username> [--email <email>]"
|
|
|
|
mkdir -p "$USERS_DIR"
|
|
|
|
# Generate user key
|
|
openssl ecparam -genkey -name prime256v1 -noout -out "$USERS_DIR/$cn.key" 2>/dev/null
|
|
|
|
# Build subject
|
|
local subj="/CN=$cn"
|
|
local san_line=""
|
|
if [ -n "$email" ]; then
|
|
san_line="email:${email}"
|
|
fi
|
|
|
|
# Create CSR
|
|
openssl req -new \
|
|
-key "$USERS_DIR/$cn.key" \
|
|
-out "$USERS_DIR/$cn.csr" \
|
|
-subj "$subj" \
|
|
2>/dev/null
|
|
|
|
# Create extensions config
|
|
local ext_file
|
|
ext_file=$(mktemp)
|
|
cat > "$ext_file" <<EOF
|
|
basicConstraints = CA:FALSE
|
|
keyUsage = critical, digitalSignature
|
|
extendedKeyUsage = clientAuth
|
|
EOF
|
|
|
|
if [ -n "$san_line" ]; then
|
|
echo "subjectAltName = ${san_line}" >> "$ext_file"
|
|
fi
|
|
|
|
# Sign with CA
|
|
openssl x509 -req \
|
|
-in "$USERS_DIR/$cn.csr" \
|
|
-CA "$CA_DIR/cluster-ca.crt" \
|
|
-CAkey "$CA_DIR/cluster-ca.key" \
|
|
-CAcreateserial \
|
|
-out "$USERS_DIR/$cn.crt" \
|
|
-days "$USER_DAYS" \
|
|
-extfile "$ext_file" \
|
|
2>/dev/null
|
|
|
|
rm -f "$USERS_DIR/$cn.csr" "$ext_file"
|
|
|
|
echo "User cert issued ($USER_DAYS days):"
|
|
echo " $USERS_DIR/$cn.crt"
|
|
echo " $USERS_DIR/$cn.key"
|
|
echo ""
|
|
echo "Import both files into the user's browser or CLI tool."
|
|
}
|
|
|
|
# ── Main dispatch ────────────────────────────────────────────────────
|
|
|
|
case "${1:-}" in
|
|
init)
|
|
shift
|
|
cmd_init "$@"
|
|
;;
|
|
issue-node)
|
|
shift
|
|
cmd_issue_node "$@"
|
|
;;
|
|
issue-user)
|
|
shift
|
|
cmd_issue_user "$@"
|
|
;;
|
|
*)
|
|
echo "Usage: $0 <command>"
|
|
echo ""
|
|
echo "Commands:"
|
|
echo " init Initialize cluster CA"
|
|
echo " issue-node --name <n> --san <addrs> Issue node cert (server+client auth)"
|
|
echo " issue-user --cn <name> [--email <e>] Issue user cert (client auth only)"
|
|
exit 1
|
|
;;
|
|
esac
|