522 lines
21 KiB
Bash
Executable File
522 lines
21 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# 50-hardening — system defaults. Each sub-step y/N, all idempotent.
|
|
|
|
step_name="hardening"
|
|
step_desc="ssh, ufw, updates, unattended, tz, hostname, swap, locale, fail2ban, angie config + built-in ACME"
|
|
step_run="run_hardening"
|
|
|
|
run_hardening() {
|
|
log "hardening system defaults"
|
|
_h_ssh
|
|
_h_ufw
|
|
_h_updates
|
|
_h_tz_hostname
|
|
_h_swap
|
|
_h_locale
|
|
_h_fail2ban
|
|
_h_angie_config
|
|
_h_angie_acme
|
|
ok "hardening pass complete"
|
|
}
|
|
|
|
# ── SSH: permit root key login, disable password auth ──────────────────────
|
|
_h_ssh() {
|
|
if ! command -v sshd >/dev/null 2>&1 && ! [[ -f /etc/ssh/sshd_config ]]; then
|
|
warn "no sshd found — skipping SSH hardening"; return
|
|
fi
|
|
if ! yn "Harden sshd: PermitRootLogin prohibit-password, disable password auth?
|
|
|
|
WARNING: only say yes if you can already log in with a public key,
|
|
else you may lock yourself out. A backup will be made and sshd -t
|
|
is run before reload. Continue?" n; then
|
|
echo " skip ssh hardening"; return
|
|
fi
|
|
|
|
local cfg="/etc/ssh/sshd_config"
|
|
sudo_ cp -a "$cfg" "${cfg}.bak.$(date +%Y%m%d-%H%M%S)"
|
|
|
|
sshd_set() { # key value — replace or append a top-level directive
|
|
local k="$1" v="$2" cfg="$3"
|
|
if grep -qiE "^\s*#?\s*${k}\b" "$cfg"; then
|
|
sudo_ sed -i -E "s|^\s*#?\s*${k}\b.*|${k} ${v}|" "$cfg"
|
|
else
|
|
echo "${k} ${v}" | sudo_ tee -a "$cfg" >/dev/null
|
|
fi
|
|
}
|
|
sshd_set PermitRootLogin prohibit-password "$cfg"
|
|
sshd_set PasswordAuthentication no "$cfg"
|
|
sshd_set KbdInteractiveAuthentication no "$cfg"
|
|
sshd_set PubkeyAuthentication yes "$cfg"
|
|
|
|
if sudo_ sshd -t 2>/dev/null; then
|
|
if command -v systemctl >/dev/null 2>&1 && systemctl is-active ssh sshd 2>/dev/null | grep -q active; then
|
|
sudo_ systemctl reload ssh 2>/dev/null || sudo_ systemctl reload sshd 2>/dev/null || sudo_ systemctl restart ssh sshd 2>/dev/null
|
|
else
|
|
sudo_ systemctl restart ssh 2>/dev/null || sudo_ systemctl restart sshd 2>/dev/null || \
|
|
sudo_ service ssh restart 2>/dev/null || sudo_ service sshd restart 2>/dev/null || warn "could not reload sshd"
|
|
fi
|
|
ok "sshd hardened (root key-only, no passwords). Keep your current session open and test a new login before closing it."
|
|
else
|
|
err "sshd -t failed — restoring backup and aborting ssh hardening"
|
|
sudo_ cp -a "${cfg}.bak.$(date +%Y%m%d-%H%M%S)" "$cfg" 2>/dev/null || true
|
|
warn "ssh hardening aborted, config restored"
|
|
fi
|
|
}
|
|
|
|
# ── ufw: deny incoming, allow 22 + mosh UDP + angie 80/443 ──────────────────
|
|
_h_ufw() {
|
|
command -v ufw >/dev/null 2>&1 || { warn "ufw not installed — skipping"; return; }
|
|
if ! yn "Enable ufw (deny incoming, allow 22, mosh 60000:61000/udp, 80+443/tcp)?" y; then
|
|
echo " skip ufw"; return
|
|
fi
|
|
sudo_ ufw --force reset >/dev/null 2>&1 || true
|
|
sudo_ ufw default deny incoming
|
|
sudo_ ufw default allow outgoing
|
|
sudo_ ufw allow 22/tcp comment 'ssh'
|
|
sudo_ ufw allow 60000:61000/udp comment 'mosh'
|
|
sudo_ ufw allow 80/tcp comment 'angie http'
|
|
sudo_ ufw allow 443/tcp comment 'angie https'
|
|
sudo_ ufw --force enable
|
|
ok "ufw enabled. status:"
|
|
sudo_ ufw status verbose | sed 's/^/ /'
|
|
}
|
|
|
|
# ── updates + unattended-upgrades (debian) ─────────────────────────────────
|
|
_h_updates() {
|
|
if ! yn "Run a system upgrade now?" y; then echo " skip updates"; return; fi
|
|
case "$DISTRO" in
|
|
arch)
|
|
sudo_ pacman -Syu --noconfirm
|
|
;;
|
|
debian)
|
|
sudo_ apt-get update -y
|
|
sudo_ apt-get upgrade -y
|
|
if yn "Enable unattended-upgrades (daily security auto-patches)?" y; then
|
|
sudo_ apt-get install -y unattended-upgrades apt-listchanges
|
|
sudo_ dpkg-reconfigure -f noninteractive unattended-upgrades 2>/dev/null || true
|
|
ok "unattended-upgrades enabled"
|
|
fi
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# ── timezone + hostname ─────────────────────────────────────────────────────
|
|
_h_tz_hostname() {
|
|
if command -v timedatectl >/dev/null 2>&1; then
|
|
if yn "Set timezone (default UTC)?" y; then
|
|
local tz="${REPLY_TZ:-}"
|
|
if (( ${ALL:-0} )); then
|
|
tz="UTC"; ok "--yes: timezone defaulting to UTC"
|
|
else
|
|
read -rp " timezone [UTC]: " tz
|
|
[[ -z "$tz" ]] && tz="UTC"
|
|
fi
|
|
sudo_ timedatectl set-timezone "$tz" && ok "timezone set to $tz"
|
|
fi
|
|
else
|
|
echo " timedatectl missing — skipping tz"
|
|
fi
|
|
|
|
if yn "Set a hostname now? (optional)" n; then
|
|
read -rp " hostname: " hn
|
|
if [[ -n "$hn" ]] && command -v hostnamectl >/dev/null 2>&1; then
|
|
sudo_ hostnamectl set-hostname "$hn" && ok "hostname set to $hn"
|
|
fi
|
|
fi
|
|
}
|
|
|
|
# ── swapfile if none and RAM is low ─────────────────────────────────────────
|
|
_h_swap() {
|
|
if [[ "$(swapon --show --noheadings | wc -l)" -gt 0 ]]; then
|
|
ok "swap already present — skipping"; return
|
|
fi
|
|
local mem_mb; mem_mb="$(awk '/MemTotal/ {printf "%d", $2/1024}' /proc/meminfo)"
|
|
if (( mem_mb > 2048 )); then
|
|
echo " RAM ${mem_mb}MB > 2GB — skipping swapfile"; return
|
|
fi
|
|
if ! yn "Create a 2G swapfile (RAM is ${mem_mb}MB)?" y; then echo " skip swap"; return; fi
|
|
sudo_ fallocate -l 2G /swapfile || sudo_ dd if=/dev/zero of=/swapfile bs=1M count=2048
|
|
sudo_ chmod 600 /swapfile
|
|
sudo_ mkswap /swapfile
|
|
sudo_ swapon /swapfile
|
|
if ! grep -q '^/swapfile' /etc/fstab; then
|
|
echo '/swapfile none swap sw 0 0' | sudo_ tee -a /etc/fstab >/dev/null
|
|
fi
|
|
ok "2G swapfile created and enabled"
|
|
}
|
|
|
|
# ── locale en_US.UTF-8 ───────────────────────────────────────────────────────
|
|
_h_locale() {
|
|
case "$DISTRO" in
|
|
arch)
|
|
if ! grep -q '^en_US.UTF-8' /etc/locale.gen 2>/dev/null; then
|
|
echo " localegen already has en_US.UTF-8 or file absent — touching"
|
|
fi
|
|
if [[ -f /etc/locale.gen ]]; then
|
|
sudo_ sed -i 's/^#en_US.UTF-8/en_US.UTF-8/' /etc/locale.gen
|
|
sudo_ locale-gen
|
|
fi
|
|
;;
|
|
debian)
|
|
if ! locale -a 2>/dev/null | grep -qi 'en_US.utf8'; then
|
|
echo 'en_US.UTF-8 UTF-8' | sudo_ tee -a /etc/locale.gen >/dev/null
|
|
sudo_ locale-gen
|
|
ok "generated en_US.UTF-8"
|
|
else
|
|
ok "en_US.UTF-8 already generated"
|
|
fi
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# ── fail2ban (best-effort) ───────────────────────────────────────────────────
|
|
_h_fail2ban() {
|
|
if ! yn "Install + enable fail2ban (sshd jail by default)?" y; then echo " skip fail2ban"; return; fi
|
|
case "$DISTRO" in
|
|
arch) sudo_ pacman -Sy --noconfirm --needed fail2ban ;;
|
|
debian) sudo_ apt-get install -y fail2ban ;;
|
|
esac
|
|
# local jail for sshd (works across distros)
|
|
sudo_ tee /etc/fail2ban/jail.local >/dev/null <<'EOF'
|
|
[DEFAULT]
|
|
backend = systemd
|
|
bantime = 1h
|
|
findtime = 10m
|
|
maxretry = 5
|
|
|
|
[sshd]
|
|
enabled = true
|
|
EOF
|
|
if command -v systemctl >/dev/null 2>&1 && systemctl is-system-running >/dev/null 2>&1; then
|
|
sudo_ systemctl enable --now fail2ban 2>/dev/null || warn "could not enable fail2ban"
|
|
fi
|
|
ok "fail2ban installed + sshd jail enabled"
|
|
}
|
|
|
|
# ── angie config: _on/ targets/ modules/ + sane root config ────────────────
|
|
_h_angie_config() {
|
|
if ! command -v angie >/dev/null 2>&1; then
|
|
echo " skip angie config (angie not installed — run the angie step first)"; return
|
|
fi
|
|
if ! yn "Set up Angie config layout (/etc/angie/_on targets modules) + sane root config?" y; then
|
|
echo " skip angie config"; return
|
|
fi
|
|
|
|
local d=/etc/angie
|
|
sudo_ install -d -m 0755 "$d/_on" "$d/targets" "$d/modules" "$d/modules/http"
|
|
|
|
# ── extract packaging-specific values from the stock angie.conf ──
|
|
# Different Angie packages set user/pid/error_log differently (Arch source
|
|
# package → /etc/nginx/nginx.conf with 'user http'; angie-bin / Debian .deb
|
|
# → /etc/angie/angie.conf with 'user angie'). Preserve them when present,
|
|
# fall back to sane defaults otherwise (defaults cover all distros here).
|
|
local oldcfg="$d/angie.conf"
|
|
local altcfg="/etc/nginx/nginx.conf" # the source AUR package's path
|
|
local stock="$oldcfg"
|
|
[[ -f "$oldcfg" ]] || stock="$altcfg"
|
|
local angie_user="" angie_pid="" angie_err="" angie_modules=""
|
|
if [[ -f "$stock" ]]; then
|
|
angie_user=$(grep -E '^\s*user\s+' "$stock" | head -1 | awk '{print $2}' | tr -d ';')
|
|
angie_pid=$(grep -E '^\s*pid\s+' "$stock" | head -1 | awk '{print $2}' | tr -d ';')
|
|
angie_err=$(grep -E '^\s*error_log\s+' "$stock" | head -1 | awk '{print $2}' | tr -d ';')
|
|
# collect any load_module lines (dynamic modules)
|
|
angie_modules=$(grep -E '^\s*load_module\s+' "$stock" || true)
|
|
fi
|
|
# sane defaults if not found
|
|
[[ -z "$angie_user" ]] && angie_user=$({ [[ "${DISTRO:-}" == "arch" ]] && echo http || echo angie; })
|
|
[[ -z "$angie_pid" ]] && angie_pid=/run/angie.pid
|
|
[[ -z "$angie_err" ]] && angie_err=/var/log/angie/error.log
|
|
|
|
# locate mime.types: angie-bin/Debian ship /etc/angie/mime.types; the Arch source
|
|
# 'angie' package instead pulls it from the 'mailcap' dep at /etc/nginx/mime.types
|
|
# (and deletes its own). Pick whichever exists so 'angie -t' doesn't fail on include.
|
|
local angie_mime=""
|
|
for m in "$d/mime.types" /etc/nginx/mime.types /etc/mime.types; do
|
|
[[ -f "$m" ]] && { angie_mime="$m"; break; }
|
|
done
|
|
[[ -z "$angie_mime" ]] && { warn "no mime.types found; installing a minimal one"; _angie_install_mime "$d/mime.types"; angie_mime="$d/mime.types"; }
|
|
# the modules/ include dir must exist or the glob fails the config test
|
|
sudo_ install -d -m 0755 "$d/modules" "$d/modules/http"
|
|
|
|
# backup the original once
|
|
if [[ -f "$oldcfg" && ! -f "$oldcfg.orig.bootstrap" ]]; then
|
|
sudo_ cp -a "$oldcfg" "$oldcfg.orig.bootstrap"
|
|
fi
|
|
|
|
# ── write our root angie.conf (once — guarded, won't clobber later edits) ──
|
|
if grep -q '# managed by bootstrap' "$oldcfg" 2>/dev/null; then
|
|
ok "$oldcfg already bootstrap-managed — leaving it (keeps your edits)"
|
|
else
|
|
sudo_ tee "$oldcfg" >/dev/null <<EOF
|
|
# angie.conf — managed by bootstrap. Original backup: angie.conf.orig.bootstrap
|
|
# Edit freely; re-running the hardening step will NOT overwrite (it writes once).
|
|
|
|
user $angie_user;
|
|
worker_processes auto;
|
|
pid $angie_pid;
|
|
error_log $angie_err warn;
|
|
|
|
# dynamic modules carried over from the original package config (if any)
|
|
$angie_modules
|
|
# top-level context snippets (stream{}, env, etc.)
|
|
include /etc/angie/modules/*.conf;
|
|
|
|
events {
|
|
worker_connections 1024;
|
|
}
|
|
|
|
http {
|
|
include $angie_mime;
|
|
default_type application/octet-stream;
|
|
|
|
log_format main '\$remote_addr - \$remote_user [\$time_local] "\$request" '
|
|
'\$status \$body_bytes_sent "\$http_referer" "\$http_user_agent"';
|
|
access_log /var/log/angie/access.log main;
|
|
|
|
sendfile on;
|
|
tcp_nopush on;
|
|
tcp_nodelay on;
|
|
keepalive_timeout 65;
|
|
types_hash_max_size 2048;
|
|
|
|
server_tokens off;
|
|
client_max_body_size 16m;
|
|
|
|
gzip on;
|
|
gzip_vary on;
|
|
gzip_proxied any;
|
|
gzip_comp_level 5;
|
|
gzip_min_length 256;
|
|
gzip_types text/plain text/css application/json application/javascript
|
|
text/xml application/xml application/xml+rss text/javascript;
|
|
|
|
# http-level snippets: upstreams, maps, real_ip, proxy defaults
|
|
include /etc/angie/modules/http/*.conf;
|
|
|
|
# enabled hosts (symlinks from targets/*). _on/*.conf only — targets/ is staging.
|
|
include /etc/angie/_on/*.conf;
|
|
}
|
|
EOF
|
|
fi
|
|
|
|
# ── http-level common snippet (real_ip + proxy defaults) ──
|
|
if [[ ! -f "$d/modules/http/00-common.conf" ]]; then
|
|
sudo_ tee "$d/modules/http/00-common.conf" >/dev/null <<'EOF'
|
|
# real_ip — trust private ranges + (commented Cloudflare). Uncomment/edit for your upstream.
|
|
set_real_ip_from 10.0.0.0/8;
|
|
set_real_ip_from 172.16.0.0/12;
|
|
set_real_ip_from 192.168.0.0/16;
|
|
set_real_ip_from 169.254.0.0/16;
|
|
# set_real_ip_from 173.245.48.0/20; # Cloudflare, see https://www.cloudflare.com/ips/
|
|
real_ip_header X-Forwarded-For;
|
|
real_ip_recursive on;
|
|
|
|
# proxy defaults (inherited by every server/location unless overridden)
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
EOF
|
|
fi
|
|
|
|
# ── default catch-all server: unmatched Host -> drop ──
|
|
if [[ ! -f "$d/_on/00-default.conf" ]]; then
|
|
# The default_server also listens on 443 to catch unknown-SNI TLS clients.
|
|
# It needs SOME cert to complete the handshake before `return 444` drops them;
|
|
# angie won't start a 443 listener without ssl_certificate. Generate a self-signed
|
|
# dummy once (CN='bootstrap-default', 10y) so unknown-SNI clients get dropped
|
|
# after connecting while real vhosts are picked by SNI.
|
|
if [[ ! -f "$d/ssl/bootstrap-default.crt" ]]; then
|
|
sudo_ install -d -m 0755 "$d/ssl"
|
|
sudo_ openssl req -x509 -newkey rsa:2048 -nodes -days 3650 -keyout "$d/ssl/bootstrap-default.key" -out "$d/ssl/bootstrap-default.crt" -subj "/CN=bootstrap-default/O=bootstrap" 2>/dev/null
|
|
fi
|
|
sudo_ tee "$d/_on/00-default.conf" >/dev/null <<'EOF'
|
|
# default_server: drop requests that match no enabled host (drive-by scanners, bare-IP)
|
|
server {
|
|
listen 80 default_server;
|
|
listen [::]:80 default_server;
|
|
listen 443 ssl default_server;
|
|
listen [::]:443 ssl default_server;
|
|
server_name _;
|
|
ssl_certificate /etc/angie/ssl/bootstrap-default.crt;
|
|
ssl_certificate_key /etc/angie/ssl/bootstrap-default.key;
|
|
return 444;
|
|
}
|
|
EOF
|
|
fi
|
|
|
|
# ── angie-enable / angie-disable helpers ──
|
|
_angie_helpers
|
|
|
|
# ── source-angie bridge: point /etc/nginx/nginx.conf at our /etc/angie layout ─
|
|
# The source AUR 'angie' package compiles --conf-path=/etc/nginx/nginx.conf and
|
|
# its service runs '/usr/bin/nginx' with no -c flag, so it ignores /etc/angie.
|
|
# When that path is where angie actually reads, back it up once and symlink it to
|
|
# our managed angie.conf so the _on/targetsmodules layout takes effect. The
|
|
# angie-bin / Debian packages already read /etc/angie/angie.conf directly.
|
|
if [[ -f /etc/nginx/nginx.conf && -f "$d/angie.conf" ]]; then
|
|
local nginxconf=/etc/nginx/nginx.conf
|
|
if [[ "$(readlink -f "$nginxconf" 2>/dev/null)" == "$d/angie.conf" ]]; then
|
|
ok "$nginxconf already symlinks to $d/angie.conf"
|
|
else
|
|
if [[ ! -f "${nginxconf}.orig.bootstrap" && ! -L "$nginxconf" ]]; then
|
|
sudo_ cp -a "$nginxconf" "${nginxconf}.orig.bootstrap"
|
|
fi
|
|
sudo_ ln -sfn "$d/angie.conf" "$nginxconf"
|
|
ok "symlinked $nginxconf -> $d/angie.conf (source-angie bridge); backup at ${nginxconf}.orig.bootstrap"
|
|
fi
|
|
fi
|
|
|
|
if sudo_ angie -t 2>&1; then
|
|
ok "angie config valid"
|
|
if command -v systemctl >/dev/null 2>&1 && systemctl is-active angie >/dev/null 2>&1; then
|
|
sudo_ systemctl reload angie 2>/dev/null && ok "angie reloaded"
|
|
else
|
|
echo " start angie when ready: sudo systemctl enable --now angie"
|
|
fi
|
|
else
|
|
err "angie -t failed — review $d/angie.conf (backup at $oldcfg.orig.bootstrap)"
|
|
fi
|
|
|
|
ok "angie layout ready: $d/{angie.conf,_on/,targets/,modules/,modules/http/}"
|
|
echo " enable a site: angie-enable <name> (after writing $d/targets/<name>.conf)"
|
|
echo " disable a site: angie-disable <name>"
|
|
|
|
# ufw already opened 80/443 in _h_ufw
|
|
}
|
|
|
|
_angie_install_mime() { # dest — write a minimal mime.types if none exists on the host
|
|
sudo_ tee "$1" >/dev/null <<'EOF'
|
|
types {
|
|
text/html html htm shtml;
|
|
text/css css;
|
|
text/xml xml;
|
|
image/gif gif;
|
|
image/jpeg jpeg jpg;
|
|
image/png png;
|
|
image/svg+xml svg svgz;
|
|
application/javascript js;
|
|
application/json json;
|
|
application/pdf pdf;
|
|
application/zip zip;
|
|
application/octet-stream bin exe dll so deb dmg iso img;
|
|
}
|
|
EOF
|
|
}
|
|
_angie_helpers() {
|
|
sudo_ tee /usr/local/bin/angie-enable >/dev/null <<'EOF'
|
|
#!/usr/bin/env bash
|
|
# angie-enable <name> — symlink targets/<name>.conf into _on/ and validate.
|
|
set -euo pipefail
|
|
name="${1:-}"
|
|
[ -n "$name" ] || { echo "usage: angie-enable <name>" >&2; exit 2; }
|
|
target="/etc/angie/targets/${name}.conf"
|
|
link="/etc/angie/_on/${name}.conf"
|
|
[ -f "$target" ] || { echo "no such target: $target" >&2; exit 1; }
|
|
ln -sfn "../targets/${name}.conf" "$link"
|
|
echo "enabled: $name -> $link"
|
|
if command -v angie >/dev/null 2>&1; then
|
|
sudo angie -t || { echo "config invalid — review before reload" >&2; exit 1; }
|
|
sudo systemctl reload angie && echo "reloaded"
|
|
fi
|
|
EOF
|
|
sudo_ tee /usr/local/bin/angie-disable >/dev/null <<'EOF'
|
|
#!/usr/bin/env bash
|
|
# angie-disable <name> — remove the _on/<name>.conf symlink and reload.
|
|
set -euo pipefail
|
|
name="${1:-}"
|
|
[ -n "$name" ] || { echo "usage: angie-disable <name>" >&2; exit 2; }
|
|
link="/etc/angie/_on/${name}.conf"
|
|
[ -L "$link" ] || [ -e "$link" ] || { echo "$name not enabled ($link missing)" >&2; exit 1; }
|
|
rm -f "$link"
|
|
echo "disabled: $name"
|
|
if command -v angie >/dev/null 2>&1; then
|
|
sudo angie -t || true
|
|
sudo systemctl reload angie && echo "reloaded"
|
|
fi
|
|
EOF
|
|
sudo_ chmod +x /usr/local/bin/angie-enable /usr/local/bin/angie-disable
|
|
}
|
|
|
|
# ── angie built-in ACME (Let's Encrypt) ────────────────────────────────────
|
|
# No certbot: Angie ships http_acme and fetches/renews certs itself.
|
|
# Bootstrap can't know your domain/email, so we ship an example TLS server
|
|
# target + an `angie-issue` helper that creates the live acme_client on demand.
|
|
_h_angie_acme() {
|
|
if ! command -v angie >/dev/null 2>&1; then
|
|
echo " skip angie ACME (angie not installed)"; return
|
|
fi
|
|
if ! yn "Ship Angie built-in ACME template + angie-issue helper?" y; then
|
|
echo " skip angie ACME"; return
|
|
fi
|
|
|
|
local d=/etc/angie
|
|
# example TLS server target — NOT auto-enabled (.example, so angie-enable won't glob it)
|
|
if [[ ! -f "$d/targets/example-https.conf.example" ]]; then
|
|
sudo_ tee "$d/targets/example-https.conf.example" >/dev/null <<'EOF'
|
|
# Template for a TLS host using Angie built-in ACME.
|
|
# Use it via the helper: angie-issue <domain> [you@email]
|
|
# Or by hand: copy to targets/<domain>.conf, fill <DOMAIN>, then: angie-enable <domain>
|
|
server {
|
|
listen 80;
|
|
listen [::]:80;
|
|
listen 443 ssl;
|
|
listen [::]:443 ssl;
|
|
http2 on;
|
|
server_name <DOMAIN>;
|
|
|
|
acme default; # add this server_name to the shared 'default' ACME certificate
|
|
ssl_certificate $acme_cert_default;
|
|
ssl_certificate_key $acme_cert_key_default;
|
|
|
|
# http -> https redirect
|
|
if ($scheme = http) { return 301 https://$host$request_uri; }
|
|
|
|
location / {
|
|
# replace with your app / proxy_pass upstream;
|
|
return 200 "angie + builtin ACME ok\n";
|
|
add_header Content-Type text/plain;
|
|
}
|
|
}
|
|
EOF
|
|
fi
|
|
|
|
sudo_ tee /usr/local/bin/angie-issue >/dev/null <<'EOF'
|
|
#!/usr/bin/env bash
|
|
# angie-issue <domain> [email] — create a TLS host backed by Angie built-in ACME.
|
|
# First call (needs email) writes /etc/angie/modules/http/acme.conf with the
|
|
# shared 'default' acme_client; every call writes targets/<domain>.conf and enables it.
|
|
# All enabled domains using `acme default` share one cert covering all their server_names.
|
|
set -euo pipefail
|
|
domain="${1:-}"; email="${2:-}"
|
|
[ -n "$domain" ] || { echo "usage: angie-issue <domain> [email]" >&2; exit 2; }
|
|
acme_conf=/etc/angie/modules/http/acme.conf
|
|
|
|
if [ ! -f "$acme_conf" ]; then
|
|
[ -n "$email" ] || { echo "first-time: also pass your email for Let's Encrypt" >&2; exit 2; }
|
|
cat > "$acme_conf" <<ACME
|
|
# managed by angie-issue — Angie built-in ACME (Let's Encrypt)
|
|
# resolver is required by the acme_client directive (http context).
|
|
resolver 1.1.1.1 8.8.8.8 valid=300s ipv6=off;
|
|
acme_client default https://acme-v02.api.letsencrypt.org/directory email=${email};
|
|
ACME
|
|
echo "wrote $acme_conf (email=${email})"
|
|
else
|
|
echo "_existing $acme_conf (shared default client)"
|
|
fi
|
|
|
|
target=/etc/angie/targets/${domain}.conf
|
|
sed -e "s|<DOMAIN>|${domain}|g" /etc/angie/targets/example-https.conf.example > "$target"
|
|
echo "wrote $target"
|
|
ln -sfn "../targets/${domain}.conf" /etc/angie/_on/${domain}.conf
|
|
echo "enabled: $domain"
|
|
sudo angie -t
|
|
sudo systemctl reload angie && echo "angie reloaded — certificate is obtained automatically."
|
|
EOF
|
|
sudo_ chmod +x /usr/local/bin/angie-issue
|
|
|
|
ok "angie ACME ready: example host at $d/targets/example-https.conf.example"
|
|
echo " issue a cert: angie-issue <domain> you@email"
|
|
echo " then reload picks it up; Angie renews automatically before expiry."
|
|
} |