myca-tools/bin/myca.sh
2025-08-20 16:14:31 +01:00

222 lines
6.4 KiB
Bash

#!/usr/bin/env bash
# Simple local CA helper for issuing server certs with SANs, verifying, and optional nginx deploy.
set -euo pipefail
MYCA_DIR="${MYCA_DIR:-/etc/ssl/myca}"
CERT_DIR="$MYCA_DIR/certs"
KEY_DIR="$MYCA_DIR/private"
CSR_DIR="$MYCA_DIR/csrs"
EXT_DIR="$MYCA_DIR/exts"
NGINX_SSL_DIR="${NGINX_SSL_DIR:-/etc/nginx/ssl}"
usage() {
cat <<'USAGE'
myca.sh COMMAND [args]
Commands:
issue NAME [--dns foo --ip 1.2.3.4 ...] [--days N] [--reuse-key] [--deploy-nginx]
Create key + CSR + SAN ext, sign with CA, write cert.
sign NAME --csr /path/to/file.csr [--dns ... --ip ...] [--days N]
Sign an existing CSR with SANs.
show NAME|/path/to/cert.crt
Show subject, SANs, validity.
verify NAME|/path/to/cert.crt
Verify against CA.
list
List issued certs.
trust-ca
Install CA into Debian or Ubuntu trust store and update.
Environment:
MYCA_DIR Default /etc/ssl/myca
NGINX_SSL_DIR Default /etc/nginx/ssl
Examples:
myca.sh issue snipe-it.lan --dns snipe-it.lan --ip 192.168.17.51 --deploy-nginx
myca.sh show snipe-it.lan
myca.sh verify snipe-it.lan
Notes:
- CA files expected: $MYCA_DIR/myCA.pem and $MYCA_DIR/myCA.key
- Avoid .local hostnames. Use .lan or a domain you control.
USAGE
}
ensure_dirs() {
install -d -m 755 "$CERT_DIR" "$CSR_DIR" "$EXT_DIR"
install -d -m 700 "$KEY_DIR"
}
have_ca() {
[[ -s "$MYCA_DIR/myCA.pem" && -s "$MYCA_DIR/myCA.key" ]] || {
echo "Missing CA: $MYCA_DIR/myCA.pem or myCA.key" >&2; exit 1;
}
}
make_ext() {
local name="$1"; shift
local dns_arr=() ip_arr=()
while [[ $# -gt 0 ]]; do
case "$1" in
--dns) dns_arr+=("$2"); shift 2;;
--ip) ip_arr+=("$2"); shift 2;;
*) echo "Unknown ext arg: $1" >&2; exit 2;;
case_esac
esac
done
local has_cn=0
for d in "${dns_arr[@]:-}"; do [[ "$d" == "$name" ]] && has_cn=1; done
[[ $has_cn -eq 0 ]] && dns_arr=("$name" "${dns_arr[@]:-}")
{
echo "basicConstraints=CA:FALSE"
echo "keyUsage=digitalSignature,keyEncipherment"
echo "extendedKeyUsage=serverAuth"
echo "subjectAltName=@alt_names"
echo "[alt_names]"
local i=1
for d in "${dns_arr[@]:-}"; do echo "DNS.$i=$d"; i=$((i+1)); done
i=1
for ip in "${ip_arr[@]:-}"; do echo "IP.$i=$ip"; i=$((i+1)); done
} > "$EXT_DIR/$name.ext"
}
gen_key_csr() {
local name="$1"
openssl genrsa -out "$KEY_DIR/$name.key" 2048 >/dev/null 2>&1
chmod 600 "$KEY_DIR/$name.key"
openssl req -new -key "$KEY_DIR/$name.key" \
-out "$CSR_DIR/$name.csr" -subj "/CN=$name" >/dev/null 2>&1
}
sign_csr() {
local name="$1" days="$2"
local serial_flag
if [[ -s "$MYCA_DIR/myCA.srl" ]]; then
serial_flag=(-CAserial "$MYCA_DIR/myCA.srl")
else
serial_flag=(-CAcreateserial)
fi
openssl x509 -req -in "$CSR_DIR/$name.csr" \
-CA "$MYCA_DIR/myCA.pem" -CAkey "$MYCA_DIR/myCA.key" \
"${serial_flag[@]}" -out "$CERT_DIR/$name.crt" -days "$days" -sha256 \
-extfile "$EXT_DIR/$name.ext" >/dev/null 2>&1
chmod 644 "$CERT_DIR/$name.crt"
}
deploy_nginx() {
local name="$1"
install -d -m 755 "$NGINX_SSL_DIR"
install -m 644 "$CERT_DIR/$name.crt" "$NGINX_SSL_DIR/$name.crt"
install -m 600 "$KEY_DIR/$name.key" "$NGINX_SSL_DIR/$name.key"
if command -v nginx >/dev/null 2>&1; then
nginx -t && systemctl reload nginx || true
fi
echo "Deployed to $NGINX_SSL_DIR/$name.{crt,key}"
}
cmd_issue() {
local name days=825 reuse_key=0 deploy=0
local dns_args=() ip_args=()
shift
[[ $# -ge 1 ]] || { usage; exit 2; }
name="$1"; shift
while [[ $# -gt 0 ]]; do
case "$1" in
--days) days="$2"; shift 2;;
--reuse-key) reuse_key=1; shift;;
--deploy-nginx) deploy=1; shift;;
--dns) dns_args+=("$2"); shift 2;;
--ip) ip_args+=("$2"); shift 2;;
*) echo "Unknown option: $1" >&2; exit 2;;
esac
done
ensure_dirs; have_ca
make_ext "$name" $(for d in "${dns_args[@]:-}"; do printf -- " --dns %q" "$d"; done) \
$(for i in "${ip_args[@]:-}"; do printf -- " --ip %q" "$i"; done)
if [[ $reuse_key -eq 0 || ! -s "$KEY_DIR/$name.key" ]]; then
gen_key_csr "$name"
else
openssl req -new -key "$KEY_DIR/$name.key" \
-out "$CSR_DIR/$name.csr" -subj "/CN=$name" >/dev/null 2>&1
fi
sign_csr "$name" "$days"
echo "Issued: $CERT_DIR/$name.crt"
[[ $deploy -eq 1 ]] && deploy_nginx "$name"
}
cmd_sign_existing() {
local name csr_path="" days=825
local dns_args=() ip_args=()
shift
[[ $# -ge 1 ]] || { usage; exit 2; }
name="$1"; shift
while [[ $# -gt 0 ]]; do
case "$1" in
--csr) csr_path="$2"; shift 2;;
--days) days="$2"; shift 2;;
--dns) dns_args+=("$2"); shift 2;;
--ip) ip_args+=("$2"); shift 2;;
*) echo "Unknown option: $1" >&2; exit 2;;
esac
done
[[ -n "$csr_path" && -s "$csr_path" ]] || { echo "CSR not found" >&2; exit 1; }
ensure_dirs; have_ca
cp -f "$csr_path" "$CSR_DIR/$name.csr"
make_ext "$name" $(for d in "${dns_args[@]:-}"; do printf -- " --dns %q" "$d"; done) \
$(for i in "${ip_args[@]:-}"; do printf -- " --ip %q" "$i"; done)
sign_csr "$name" "$days"
echo "Signed: $CERT_DIR/$name.crt"
}
cmd_show() {
local target="${1:-}"
[[ -n "$target" ]] || { usage; exit 2; }
[[ "$target" == *.crt || "$target" == *.pem ]] || target="$CERT_DIR/$target.crt"
[[ -s "$target" ]] || { echo "Cert not found: $target" >&2; exit 1; }
openssl x509 -in "$target" -noout -subject -issuer -dates
echo "SANs:"
openssl x509 -in "$target" -noout -text | awk '/Subject Alternative Name/{f=1;next}/X509v3/{f=0}f'
}
cmd_verify() {
local target="${1:-}"
[[ -n "$target" ]] || { usage; exit 2; }
[[ "$target" == *.crt || "$target" == *.pem ]] || target="$CERT_DIR/$target.crt"
[[ -s "$target" ]] || { echo "Cert not found: $target" >&2; exit 1; }
have_ca
openssl verify -CAfile "$MYCA_DIR/myCA.pem" "$target"
}
cmd_list() {
ensure_dirs
ls -1 "$CERT_DIR"/*.crt 2>/dev/null || true
}
cmd_trust_ca() {
have_ca
install -m 644 "$MYCA_DIR/myCA.pem" /usr/local/share/ca-certificates/myCA.crt
update-ca-certificates
echo "CA installed to system trust."
}
main() {
[[ $# -ge 1 ]] || { usage; exit 2; }
case "$1" in
issue) cmd_issue "$@";;
sign) cmd_sign_existing "$@";;
show) shift; cmd_show "${1:-}";;
verify) shift; cmd_verify "${1:-}";;
list) cmd_list;;
trust-ca) cmd_trust_ca;;
-h|--help|help) usage;;
*) echo "Unknown command: $1" >&2; usage; exit 2;;
esac
}
main "$@"