From 62f2690fb5ac59ea4dc17b5fbe715e2ae1fd8a09 Mon Sep 17 00:00:00 2001 From: sysadmin Date: Wed, 20 Aug 2025 16:14:31 +0100 Subject: [PATCH] Initial commit --- .gitignore | 4 + README.md | 252 +++++++++++++++++++++++++++ bin/init-ca.sh | 25 +++ bin/mknginx-sni.sh | 44 +++++ bin/myca.sh | 221 +++++++++++++++++++++++ examples/vhost-snipe-it.conf.example | 33 ++++ install.sh | 10 ++ uninstall.sh | 5 + 8 files changed, 594 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 bin/init-ca.sh create mode 100644 bin/mknginx-sni.sh create mode 100644 bin/myca.sh create mode 100644 examples/vhost-snipe-it.conf.example create mode 100644 install.sh create mode 100644 uninstall.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c55a5b9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.swo +*.swp +.DS_Store + diff --git a/README.md b/README.md new file mode 100644 index 0000000..10ef388 --- /dev/null +++ b/README.md @@ -0,0 +1,252 @@ +# myca-tools + +Bash helpers for running a simple local root CA, issuing TLS certificates with SANs, and generating Nginx HTTPS reverse-proxy vhosts for internal services. + +Do not use `.local`. Use `.lan` or a domain you control. + +## Placeholders used in this README + +- `NAME` + The service hostname you want to secure. Example: `app.lan`. + +- `HOST_IP` + The local IP address of the server that presents the certificate. In a typical setup this is the machine running Nginx and hosting the service. Example: `192.168.1.50`. + +- `BACKEND_HOST:PORT` + Where Nginx should proxy. Example: `127.0.0.1:8080`. + +Replace these with your values before running commands. + +## Features + +- Initialise a root CA in `/etc/ssl/myca` +- Issue server certificates with DNS and IP SANs +- Sign existing CSRs +- Show and verify certificates +- Deploy certs to `/etc/nginx/ssl` and reload Nginx +- Generate a standard HTTPS reverse proxy vhost + +## Requirements + +- Debian or Ubuntu +- `openssl` +- `nginx` if you want reverse proxying +- Root access + +## Install + +```bash +git clone myca-tools +cd myca-tools +./install.sh +``` + +Installs: + +/usr/local/sbin/myca.sh + +/usr/local/sbin/mknginx-sni.sh + +/usr/local/sbin/init-ca.sh + +Prepares /etc/ssl/myca with certs, csrs, exts, and private. + +## Quick start + +1. Initialise a root CA if you do not already have one. + +```bash +sudo init-ca.sh "/CN=My Local Root CA" +``` + +2. Trust the CA on this host. + +```bash +sudo myca.sh trust-ca +``` + +3. Issue a cert for your internal service and deploy it to Nginx. +```bash +# Replace NAME and HOST_IP +sudo myca.sh issue NAME \ + --dns NAME --ip HOST_IP --deploy-nginx +``` + +4. Create an Nginx vhost that uses the cert and proxies to your backend. +```bash +# Replace NAME and BACKEND_HOST:PORT +sudo mknginx-sni.sh NAME BACKEND_HOST:PORT +``` + +5. Ensure clients resolve the name. +```bash +HOST_IP NAME +``` + +6. Test. +```bash +myca.sh show NAME +myca.sh verify NAME +curl -Ik https://NAME +``` + +## Notes on SANs + +You can omit --ip HOST_IP if clients always connect by DNS name and do not pin the IP. + +If clients connect by IP, include that IP in SANs with --ip. + +You can supply multiple --dns and --ip flags. + +Scripts +myca.sh + +Helper for issuing, signing, showing, verifying, and deploying certificates. +```bash +myca.sh COMMAND [args] + +Commands: + issue NAME [--dns foo --ip 1.2.3.4 ...] [--days N] [--reuse-key] [--deploy-nginx] + sign NAME --csr /path/to/file.csr [--dns ... --ip ...] [--days N] + show NAME | /path/to/cert.crt + verify NAME | /path/to/cert.crt + list + trust-ca + +Environment: + MYCA_DIR default /etc/ssl/myca + NGINX_SSL_DIR default /etc/nginx/ssl +``` + +Examples: +```bash +# Issue new key and cert with SANs +sudo myca.sh issue internal-api.lan --dns internal-api.lan --ip HOST_IP + +# Reissue keeping the same key and deploy to Nginx +sudo myca.sh issue wiki.lan --dns wiki.lan --reuse-key --deploy-nginx + +# Sign an existing CSR +sudo myca.sh sign portal.lan --csr /tmp/portal.csr --dns portal.lan --ip 10.0.0.5 + +# Show and verify +myca.sh show wiki.lan +myca.sh verify wiki.lan + +# List issued certs +myca.sh list +``` + +Files are written to: + +Keys /etc/ssl/myca/private/NAME.key + +CSRs /etc/ssl/myca/csrs/NAME.csr + +Certs /etc/ssl/myca/certs/NAME.crt + +SAN /etc/ssl/myca/exts/NAME.ext + +CA files expected: + +/etc/ssl/myca/myCA.pem + +/etc/ssl/myca/myCA.key + +/etc/ssl/myca/myCA.srl created on first issuance + +mknginx-sni.sh + +Generate a standard HTTPS reverse proxy server block for a given name and backend. +```bash +sudo mknginx-sni.sh NAME [BACKEND_HOST:PORT] +# writes /etc/nginx/conf.d/NAME.conf +# proxies to BACKEND, default 127.0.0.1:8000 +# uses /etc/nginx/ssl/NAME.crt and /etc/nginx/ssl/NAME.key +``` + +Example: +```bash +sudo mknginx-sni.sh internal-api.lan 127.0.0.1:9000 +``` + +init-ca.sh + +Initialise a new root CA in /etc/ssl/myca. +```bash +sudo init-ca.sh "/CN=My Local Root CA" +``` + +Creates: + +Root key /etc/ssl/myca/myCA.key 4096 bit + +Root cert /etc/ssl/myca/myCA.pem valid 10 years + +Trusting your CA on clients + +Linux Debian or Ubuntu: +```bash +sudo cp /etc/ssl/myca/myCA.pem /usr/local/share/ca-certificates/myCA.crt +sudo update-ca-certificates +``` + +ndows: + +Import myCA.pem into Local Computer, Trusted Root Certification Authorities. + +macOS: + +Import myCA.pem into the System keychain in Keychain Access. Set Always Trust. + +Browsers must trust the CA or you will see warnings. + +Directory layout +```vbnet +/etc/ssl/myca/ +├── myCA.key root key +├── myCA.pem root certificate +├── myCA.srl serial file +├── certs/ issued certificates +├── csrs/ certificate signing requests +├── exts/ SAN extension files +└── private/ server private keys +``` + +Nginx deploy target: +```swift +/etc/nginx/ssl/NAME.crt +/etc/nginx/ssl/NAME.key +``` + +Environment variables + +Override defaults if needed: +```bash +export MYCA_DIR=/srv/pki +export NGINX_SSL_DIR=/etc/nginx/ssl +``` + +Security + +Back up /etc/ssl/myca/myCA.key offline. Losing it prevents renewal under the same root. + +Keep /etc/ssl/myca/private permissions intact. Keys are mode 600. + +Keep this root CA for internal use only. Do not use it on the public internet. + +Use shorter lifetimes for anything exposed beyond your LAN. + +Troubleshooting + +Missing CA: ensure myCA.pem and myCA.key exist under /etc/ssl/myca. + +verify error:num=20: the client does not trust your CA. Install the CA to the client trust store. + +Browser still warns after trusting CA: clear the site’s cached cert or restart the browser. + +Nginx reload fails: run nginx -t and fix the reported error. + +Licence + +MIT. diff --git a/bin/init-ca.sh b/bin/init-ca.sh new file mode 100644 index 0000000..be52eb5 --- /dev/null +++ b/bin/init-ca.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +# Initialise a simple local root CA in /etc/ssl/myca +set -euo pipefail +MYCA_DIR="${MYCA_DIR:-/etc/ssl/myca}" +CN="${1:-/CN=Local Development Root CA}" +DAYS="${DAYS:-3650}" + +install -d -m 755 "$MYCA_DIR" "$MYCA_DIR/certs" "$MYCA_DIR/csrs" "$MYCA_DIR/exts" +install -d -m 700 "$MYCA_DIR/private" + +if [[ -e "$MYCA_DIR/myCA.key" || -e "$MYCA_DIR/myCA.pem" ]]; then + echo "CA already exists in $MYCA_DIR" >&2 + exit 1 +fi + +openssl genrsa -out "$MYCA_DIR/myCA.key" 4096 +chmod 600 "$MYCA_DIR/myCA.key" + +openssl req -x509 -new -nodes -key "$MYCA_DIR/myCA.key" -sha256 -days "$DAYS" \ + -out "$MYCA_DIR/myCA.pem" -subj "$CN" + +chmod 644 "$MYCA_DIR/myCA.pem" +echo "Root CA created: $MYCA_DIR/myCA.pem" +echo "Private key: $MYCA_DIR/myCA.key" + diff --git a/bin/mknginx-sni.sh b/bin/mknginx-sni.sh new file mode 100644 index 0000000..94dffd6 --- /dev/null +++ b/bin/mknginx-sni.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# Generate a standard SSL reverse proxy server block for NAME and backend HOST:PORT. +set -euo pipefail +name="${1:-}"; backend="${2:-127.0.0.1:8000}" +[[ -n "$name" ]] || { echo "Usage: mknginx-sni.sh NAME [BACKEND_HOST:PORT]"; exit 2; } + +cat >/etc/nginx/conf.d/"$name".conf <&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 "$@" + diff --git a/examples/vhost-snipe-it.conf.example b/examples/vhost-snipe-it.conf.example new file mode 100644 index 0000000..5e80b1a --- /dev/null +++ b/examples/vhost-snipe-it.conf.example @@ -0,0 +1,33 @@ +upstream snipe_it_lan_app { server 127.0.0.1:8000; keepalive 32; } + +server { + listen 80; + server_name snipe-it.lan; + return 301 https://$host$request_uri; +} + +server { + listen 443 ssl http2; + server_name snipe-it.lan; + + ssl_certificate /etc/nginx/ssl/snipe-it.lan.crt; + ssl_certificate_key /etc/nginx/ssl/snipe-it.lan.key; + + client_max_body_size 100m; + + location / { + proxy_pass http://snipe_it_lan_app; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Proto https; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + } +} + +map $http_upgrade $connection_upgrade { + default upgrade; + '' close; +} + diff --git a/install.sh b/install.sh new file mode 100644 index 0000000..3a90e2d --- /dev/null +++ b/install.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +set -euo pipefail +prefix="/usr/local/sbin" +sudo install -d -m 700 /etc/ssl/myca/private +sudo install -d -m 755 /etc/ssl/myca/{certs,csrs,exts} +sudo install -m 755 bin/myca.sh "$prefix/myca.sh" +sudo install -m 755 bin/mknginx-sni.sh "$prefix/mknginx-sni.sh" +sudo install -m 755 bin/init-ca.sh "$prefix/init-ca.sh" +echo "Installed to $prefix. CA dir at /etc/ssl/myca" + diff --git a/uninstall.sh b/uninstall.sh new file mode 100644 index 0000000..68a33ac --- /dev/null +++ b/uninstall.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail +sudo rm -f /usr/local/sbin/{myca.sh,mknginx-sni.sh,init-ca.sh} +echo "Removed scripts. CA materials under /etc/ssl/myca left untouched." +