diff --git a/Bash/Proxmox Scripts/Ubuntu Cloud Template/README.md b/Bash/Proxmox Scripts/Ubuntu Cloud Template/README.md index b5d8bb1..2950ec2 100644 --- a/Bash/Proxmox Scripts/Ubuntu Cloud Template/README.md +++ b/Bash/Proxmox Scripts/Ubuntu Cloud Template/README.md @@ -128,13 +128,9 @@ placeholder values from `shared.conf.example`. `GMAIL_USER` and `GMAIL_APP_PASSWORD` still work as the old names for `MAIL_RELAY_USER` and `MAIL_RELAY_PASSWORD`, with a warning. -`verify-template.sh` and `verify-clone.sh` still expect the Gmail relay, so -for `custom` or `none` expect FAILs in their mail section until they read -`shared.conf`. - - The brackets stop MX lookups. -- IPv4 only, TLS where the relay supports it, and queued mail expires after - one day, so a failure defers and drops instead of looping. +- IPv4 only, TLS (required on 587, 465 or with a login), and queued mail + expires after one day, so a failure defers and drops instead of looping. - A per-boot script sets `/etc/mailname` and `myhostname` from the clone's own hostname plus `MAIL_DOMAIN`, so clones never keep the template's name. - Mail is sent as the natural `user@hostname.domain`. There is no sender or @@ -160,7 +156,9 @@ Options (see `--help` for all of them): - `--profile` and `--send-test-mail
` are passed to `verify-template.sh`. With the mail option the script also waits up to 60 - seconds for Postfix to log `status=sent` for that address. + seconds for Postfix to log `status=sent` for that address. With + `MAIL_RELAY=none` it does not wait (mail stays on the clone). +- `--config `: the `shared.conf` to read (see below). - `--vmid `: VMID for the clone. Default: the next free one. A VMID that already exists is refused. - `--full [--storage ]`: full clone. Default is a linked clone, which @@ -169,6 +167,16 @@ Options (see `--help` for all of them): - `--timeout ` and `--verify-timeout `: waits for the agent and cloud-init (default 300), and for the verify run (default 600). +What to expect comes from `shared.conf`, found as `build-templates.sh` finds +it: `--config`, then `SHARED_CONF`, then `BUILD_CONF`, then `shared.conf` next +to the script. A file named by one of the first three must exist. The file is +read in a subshell, and only non-secret settings go to the guest, as +`VERIFY_*` variables: the mail relay, host, port and login name, `MAIL_ROOT_TO`, +`MAIL_SENDER`, `MAIL_DOMAIN`, `UU_MAIL`, the ansible key, `LOGIN_SHELL`, +`NEEDRESTART_MODE` and the journal cap. The relay password and the other +secrets are never read out or sent. With no `shared.conf` found, it says so and +`verify-template.sh` uses its built-in defaults. + The template needs `qemu-guest-agent` installed and the VM option `agent=1`. The clone is destroyed on success, failure and Ctrl-C. Only a VM this run created is ever stopped or destroyed. Exit codes: 0 passed, 1 a FAIL or a @@ -191,9 +199,11 @@ Options: The exit code is 0 when there are no FAILs (warnings are fine). -`verify-template.sh` has the values it expects (domain, Gmail user, SSH key, -time zone and so on) at the top. Edit them to match your `shared.conf` and -profiles. +Run by hand, `verify-template.sh` uses the values at its top (mail relay, +domain, relay login, SSH key, time zone and so on). Edit them to match your +`shared.conf` and profiles, or use `verify-clone.sh`, which passes them from +`shared.conf`. Settings left blank in `shared.conf` (no relay login, no +`UU_MAIL`, no `LOGIN_SHELL` and so on) are shown as `SKIP` or checked as off. ## Manual steps diff --git a/Bash/Proxmox Scripts/Ubuntu Cloud Template/verify-clone.sh b/Bash/Proxmox Scripts/Ubuntu Cloud Template/verify-clone.sh index fc4b525..55368a2 100755 --- a/Bash/Proxmox Scripts/Ubuntu Cloud Template/verify-clone.sh +++ b/Bash/Proxmox Scripts/Ubuntu Cloud Template/verify-clone.sh @@ -7,6 +7,10 @@ # it, boots the clone, waits for the guest agent and cloud-init, runs # verify-template.sh inside it through the agent, then destroys the clone. # +# The values to expect come from shared.conf (found as build-templates.sh +# finds it). Only non-secret settings are passed to the guest, never a +# password. No shared.conf: verify-template.sh's own defaults are used. +# # Usage (as root, on the Proxmox host): # ./verify-clone.sh [options] # ./verify-clone.sh --help # full help, no root needed @@ -42,6 +46,9 @@ Clones the template, starts the clone, waits for the guest agent and for cloud-init to finish, runs verify-template.sh inside it through the guest agent, prints the result, then destroys the clone. +What to expect (mail relay, domain, ansible key, shell, and so on) is +read from shared.conf, so the check matches what the builder made. + USAGE ./verify-clone.sh [options] ./verify-clone.sh -h|--help|help @@ -52,7 +59,11 @@ ARGUMENTS verify-template.sh. Default: it works it out. --send-test-mail Passed to verify-template.sh, which sends a test mail. This script then waits up to 60s - for Postfix to log status=sent for it. + for Postfix to log status=sent for it + (not with MAIL_RELAY=none: mail stays local). + --config shared.conf to read. Default: SHARED_CONF, + then BUILD_CONF, then shared.conf next to + this script. --vmid VMID for the clone. Must not exist yet. Default: the next free one (pvesh nextid). --full Full clone. Default: linked clone. @@ -82,6 +93,17 @@ EXAMPLES ./verify-clone.sh 9002 --send-test-mail you@example.com ./verify-clone.sh 9002 --full --storage local-lvm --vmid 9101 ./verify-clone.sh 9002 --keep + ./verify-clone.sh 9002 --config /root/other.conf + +SHARED.CONF + Read the same way as build-templates.sh, with the same defaults and + old names (GMAIL_USER). Only these are passed to the guest, as VERIFY_* + variables: MAIL_RELAY, MAIL_RELAY_HOST, MAIL_RELAY_PORT, + MAIL_RELAY_USER, MAIL_ROOT_TO, MAIL_SENDER, MAIL_DOMAIN, UU_MAIL, + ANSIBLE_SSH_PUBKEY, LOGIN_SHELL, NEEDRESTART_MODE, JOURNAL_MAX_MB and + JOURNAL_MAX_PERCENT. No password or other secret is read out or sent. + A file named by --config, SHARED_CONF or BUILD_CONF must exist. With no + shared.conf next to this script, verify-template.sh's defaults apply. NOTES - Run as root on the Proxmox host. @@ -97,7 +119,7 @@ EOF # ============================================================================= # ARGUMENTS # ============================================================================= -TEMPLATE="" PROFILE="" TEST_MAIL="" NEWID="" FULL=0 STORAGE="" KEEP=0 +TEMPLATE="" PROFILE="" TEST_MAIL="" NEWID="" FULL=0 STORAGE="" KEEP=0 CONFIG="" TIMEOUT=300 VERIFY_TIMEOUT=600 VERIFY_SCRIPT="$SCRIPT_DIR/verify-template.sh" usage_error() { { echo "Error: $*"; echo "Run ./verify-clone.sh --help for usage."; } >&2; exit 2; } @@ -115,6 +137,7 @@ while [[ $# -gt 0 ]]; do --timeout) need_val "$@"; TIMEOUT="$2"; shift 2 ;; --verify-timeout) need_val "$@"; VERIFY_TIMEOUT="$2"; shift 2 ;; --script) need_val "$@"; VERIFY_SCRIPT="$2"; shift 2 ;; + --config) need_val "$@"; CONFIG="$2"; shift 2 ;; -*) usage_error "unknown option: $1" ;; *) [[ -z "$TEMPLATE" ]] || usage_error "more than one template VMID given." TEMPLATE="$1"; shift ;; @@ -158,6 +181,73 @@ grep -q '^# verify-template.sh$' "$VERIFY_SCRIPT" \ size=$(stat -c %s "$VERIFY_SCRIPT") (( size <= MAX_STDIN )) || die "$VERIFY_SCRIPT is $size bytes: qm guest exec takes at most $MAX_STDIN on stdin." 2 +# ============================================================================= +# SHARED CONFIG +# Found as build-templates.sh finds it (the old build-templates.conf is not +# looked for). Sourced in a subshell that prints only the allow-listed, +# non-secret values, NUL-separated: no password reaches this shell, the +# qm command line or the guest. +# ============================================================================= +read_conf() { # : prints VERIFY_x=value NUL ..., ending with VERIFY_FROM_CONFIG=1 + ( + set +u + # The builder's defaults, so a setting left out means the same here. + MAIL_RELAY="gmail" MAIL_RELAY_HOST="" MAIL_RELAY_PORT="" MAIL_RELAY_USER="" + MAIL_ROOT_TO="" MAIL_SENDER="" MAIL_DOMAIN="" GMAIL_USER="" UU_MAIL="" + ANSIBLE_SSH_PUBKEY="" LOGIN_SHELL="/usr/bin/zsh" NEEDRESTART_MODE="a" + JOURNAL_MAX_MB="1024" JOURNAL_MAX_PERCENT="10" + # shellcheck source=/dev/null + source "$1" >/dev/null 2>&1 + unset MAIL_RELAY_PASSWORD GMAIL_APP_PASSWORD CLOUD_PASSWORD WAKATIME_CFG + # Old name, relay preset and root alias, as in build-templates.sh. + [[ -n "$MAIL_RELAY_USER" ]] || MAIL_RELAY_USER="$GMAIL_USER" + MAIL_RELAY="${MAIL_RELAY:-gmail}" + [[ "$MAIL_RELAY" == gmail ]] && { MAIL_RELAY_HOST="smtp.gmail.com"; MAIL_RELAY_PORT="587"; } + if [[ "$MAIL_RELAY" != none && -z "$MAIL_ROOT_TO" && "$MAIL_RELAY_USER" == *@* ]]; then + MAIL_ROOT_TO="$MAIL_RELAY_USER" + fi + # if_set: blank = verify-template.sh's default. always: blank = "off". + if_set() { [[ -z "$2" ]] || printf '%s=%s\0' "$1" "$2"; } + always() { printf '%s=%s\0' "$1" "$2"; } + always VERIFY_MAIL_RELAY "$MAIL_RELAY" + if_set VERIFY_RELAY_HOST "$MAIL_RELAY_HOST" + if_set VERIFY_RELAY_PORT "$MAIL_RELAY_PORT" + always VERIFY_RELAY_USER "$MAIL_RELAY_USER" + always VERIFY_ROOT_TO "$MAIL_ROOT_TO" + always VERIFY_MAIL_SENDER "$MAIL_SENDER" + if_set VERIFY_MAIL_DOMAIN "$MAIL_DOMAIN" + always VERIFY_UU_MAIL "$UU_MAIL" + if_set VERIFY_ANSIBLE_KEY "$ANSIBLE_SSH_PUBKEY" + always VERIFY_SHELL "$LOGIN_SHELL" + always VERIFY_NEEDRESTART "$NEEDRESTART_MODE" + always VERIFY_JOURNAL_MAX_MB "$JOURNAL_MAX_MB" + if_set VERIFY_JOURNAL_PERCENT "$JOURNAL_MAX_PERCENT" + printf 'VERIFY_FROM_CONFIG=1\0' + ) +} + +if [[ -n "$CONFIG" ]]; then CONF_FILE="$CONFIG" +elif [[ -n "${SHARED_CONF:-}" ]]; then CONF_FILE="$SHARED_CONF" +elif [[ -n "${BUILD_CONF:-}" ]]; then CONF_FILE="$BUILD_CONF" +elif [[ -f "$SCRIPT_DIR/shared.conf" ]]; then CONF_FILE="$SCRIPT_DIR/shared.conf" +else CONF_FILE=""; fi + +conf_env=() RELAY="" +if [[ -n "$CONF_FILE" ]]; then + [[ -f "$CONF_FILE" && -r "$CONF_FILE" ]] || die "shared.conf not found or not readable: $CONF_FILE" 2 + # Output dropped: an error message could quote a line holding a password. + bash -n "$CONF_FILE" &>/dev/null || die "$CONF_FILE has a syntax error (check it with: bash -n)." 2 + mapfile -d '' -t conf_env < <(read_conf "$CONF_FILE") + (( ${#conf_env[@]} )) && [[ "${conf_env[-1]}" == VERIFY_FROM_CONFIG=1 ]] \ + || die "could not read $CONF_FILE." 2 + for kv in "${conf_env[@]}"; do [[ "$kv" == VERIFY_MAIL_RELAY=* ]] && RELAY="${kv#*=}"; done + [[ "$RELAY" =~ ^(gmail|custom|none)$ ]] \ + || die "MAIL_RELAY in $CONF_FILE must be gmail, custom or none, not '$RELAY'." 2 + info "Expected values from $CONF_FILE (non-secret settings only)" +else + info "No shared.conf found: verify-template.sh's built-in defaults apply (or use --config )." +fi + tpl_conf="$("$QM" config "$TEMPLATE" 2>&1)" || die "VM $TEMPLATE not found: $tpl_conf" 2 grep -qx 'template: 1' <<< "$tpl_conf" || die "VM $TEMPLATE is not a template." 2 grep -qE '^agent: *(1|enabled=1)' <<< "$tpl_conf" \ @@ -329,16 +419,22 @@ esac verify_args=() [[ -n "$PROFILE" ]] && verify_args+=(--profile "$PROFILE") [[ -n "$TEST_MAIL" ]] && verify_args+=(--send-test-mail "$TEST_MAIL") +# The expected values go in as env arguments: qm passes each one as its +# own argument, so no shell quoting is involved. +guest_cmd=(/bin/bash -s -- "${verify_args[@]}") +(( ${#conf_env[@]} )) && guest_cmd=(env "${conf_env[@]}" "${guest_cmd[@]}") info "Running $(basename "$VERIFY_SCRIPT") in $NEWID (up to ${VERIFY_TIMEOUT}s)" echo -e "${B}----- guest output -----${N}" -guest_exec "$NEWID" "$VERIFY_TIMEOUT" "$VERIFY_SCRIPT" -- /bin/bash -s -- "${verify_args[@]}" +guest_exec "$NEWID" "$VERIFY_TIMEOUT" "$VERIFY_SCRIPT" -- "${guest_cmd[@]}" RC=$? echo -e "${B}----- end of guest output -----${N}" (( RC == 5 )) && exit 5 # Postfix sends in the background: wait for the relay's verdict. -if [[ -n "$TEST_MAIL" ]]; then +if [[ -n "$TEST_MAIL" && "$RELAY" == none ]]; then + echo -e " ${Y}WARN${N} not waiting for the test mail: MAIL_RELAY=none keeps mail on the clone" +elif [[ -n "$TEST_MAIL" ]]; then info "Waiting for Postfix to relay the test mail (up to ${MAIL_WAIT}s)" mail_deadline=$((SECONDS + MAIL_WAIT)) while :; do diff --git a/Bash/Proxmox Scripts/Ubuntu Cloud Template/verify-template.sh b/Bash/Proxmox Scripts/Ubuntu Cloud Template/verify-template.sh index 4ddd6c8..82c9605 100755 --- a/Bash/Proxmox Scripts/Ubuntu Cloud Template/verify-template.sh +++ b/Bash/Proxmox Scripts/Ubuntu Cloud Template/verify-template.sh @@ -13,25 +13,35 @@ # --send-test-mail Also sends the postfix test email (the one manual step). # # Exit code: 0 = no FAILs (WARNs are fine), 1 = at least one FAIL. -# Holds no secrets: it checks the Gmail password is set, never what it is. +# Holds no secrets: it checks the relay password is set, never what it is. +# +# verify-clone.sh passes the shared.conf values as VERIFY_* variables. +# Run by hand, the defaults below are used. Set but empty = that setting +# is off (checked as off, or SKIP). # ============================================================================= set -uo pipefail # --- What the templates are meant to have (match shared.conf and profiles/*.conf) --- +# ${VERIFY_x-default}: an empty VERIFY_x is kept (the setting is off). EXPECT_LANG="en_GB.UTF-8" EXPECT_TZ="Europe/London" EXPECT_KEYMAP="gb" -EXPECT_MAIL_DOMAIN="modem7.com" -EXPECT_GMAIL_USER="modem7@gmail.com" -EXPECT_UU_MAIL="modem7@gmail.com" -EXPECT_ANSIBLE_KEY="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFv+XL86DUoq1o4EEC24uYecv4nU76XcmTeOO1rin9gx ansible" -EXPECT_SHELL="/usr/bin/zsh" -EXPECT_NEEDRESTART="a" -EXPECT_JOURNAL_MAX_MB=1024 -EXPECT_JOURNAL_PERCENT=10 +EXPECT_MAIL_DOMAIN="${VERIFY_MAIL_DOMAIN-modem7.com}" +EXPECT_RELAY="${VERIFY_MAIL_RELAY-gmail}" # gmail, custom or none +EXPECT_RELAY_HOST="${VERIFY_RELAY_HOST-smtp.gmail.com}" +EXPECT_RELAY_PORT="${VERIFY_RELAY_PORT-587}" +EXPECT_RELAY_USER="${VERIFY_RELAY_USER-modem7@gmail.com}" # empty = no relay login +EXPECT_ROOT_TO="${VERIFY_ROOT_TO-modem7@gmail.com}" # empty = no root alias +EXPECT_SENDER="${VERIFY_MAIL_SENDER-}" # empty = no sender rewrite +EXPECT_UU_MAIL="${VERIFY_UU_MAIL-modem7@gmail.com}" # empty = no upgrade mail +EXPECT_ANSIBLE_KEY="${VERIFY_ANSIBLE_KEY-ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFv+XL86DUoq1o4EEC24uYecv4nU76XcmTeOO1rin9gx ansible}" +EXPECT_SHELL="${VERIFY_SHELL-/usr/bin/zsh}" # empty = left as bash +EXPECT_NEEDRESTART="${VERIFY_NEEDRESTART-a}" # empty = OS default +EXPECT_JOURNAL_MAX_MB="${VERIFY_JOURNAL_MAX_MB-1024}" # empty = OS default +EXPECT_JOURNAL_PERCENT="${VERIFY_JOURNAL_PERCENT-10}" EXPECT_TUNED="virtual-guest" -PKGS_CORE="aptitude auto-apt-proxy libsasl2-modules tuned tuned-utils unattended-upgrades zsh +PKGS_CORE="aptitude auto-apt-proxy tuned tuned-utils unattended-upgrades zsh qemu-guest-agent cloud-init cloud-utils cloud-guest-utils postfix bsd-mailx" PKGS_TOOLS="acl bind9-dnsutils cifs-utils curl git htop needrestart net-tools plocate unzip" PKGS_DESKTOP="ubuntu-desktop-minimal gnome-remote-desktop openssl" @@ -45,17 +55,30 @@ while [[ $# -gt 0 ]]; do case "$1" in --profile) PROFILE="${2:-}"; shift 2 ;; --send-test-mail) TEST_MAIL="${2:-}"; shift 2 ;; - -h|--help) sed -n '2,16p' "$0"; exit 0 ;; + -h|--help) sed -n '2,20p' "$0"; exit 0 ;; *) echo "Unknown argument: $1" >&2; exit 2 ;; esac done +[[ "$EXPECT_RELAY" =~ ^(gmail|custom|none)$ ]] \ + || { echo "VERIFY_MAIL_RELAY must be gmail, custom or none, not '$EXPECT_RELAY'." >&2; exit 2; } +# Checked as numbers: they go into $(( )). +[[ "$EXPECT_JOURNAL_MAX_MB" =~ ^[0-9]*$ && "$EXPECT_JOURNAL_PERCENT" =~ ^[0-9]+$ ]] \ + || { echo "VERIFY_JOURNAL_MAX_MB and VERIFY_JOURNAL_PERCENT must be whole numbers." >&2; exit 2; } [[ $EUID -eq 0 ]] || { echo "Run as root (sudo $0)." >&2; exit 2; } +# The relay logs in (SASL) only with a user, as in build-templates.sh. +RELAY_LOGIN=0 +[[ "$EXPECT_RELAY" != none && -n "$EXPECT_RELAY_USER" ]] && RELAY_LOGIN=1 +(( RELAY_LOGIN )) && PKGS_CORE+=" libsasl2-modules" +LOGIN_NAME="relay login" +[[ "$EXPECT_RELAY" == gmail ]] && LOGIN_NAME="Gmail login" + if [[ -t 1 ]]; then G='\033[0;32m' Y='\033[1;33m' R='\033[0;31m' B='\033[1m' N='\033[0m'; else G='' Y='' R='' B='' N=''; fi PASSES=0 WARNS=0 FAILS=0 pass() { echo -e " ${G}PASS${N} $*"; PASSES=$((PASSES + 1)); } warn() { echo -e " ${Y}WARN${N} $*"; WARNS=$((WARNS + 1)); } fail() { echo -e " ${R}FAIL${N} $*"; FAILS=$((FAILS + 1)); } +skip() { echo -e " SKIP $*"; } # not counted: the setting is off on purpose section() { echo -e "\n${B}== $* ==${N}"; } # check "" PASS if the command succeeds check() { local d="$1"; shift; if "$@" &>/dev/null; then pass "$d"; else fail "$d"; fi; } @@ -89,6 +112,11 @@ fi echo -e "${B}Template check: $(hostname) - profile: ${PROFILE}${AUTO}${N}" echo "Login user: ${LOGIN_USER:-(none found)} Kernel: $(uname -r)" +if [[ "${VERIFY_FROM_CONFIG:-}" == 1 ]]; then + echo "Expected values: from shared.conf (mail relay: $EXPECT_RELAY)" +else + echo "Expected values: built-in defaults or VERIFY_* (mail relay: $EXPECT_RELAY)" +fi # ============================================================================= section "First boot" @@ -247,10 +275,30 @@ check "postfix running" systemctl is-active --quiet postfix check "postfix config valid (postfix check)" postfix check pc() { postconf -h "$1" 2>/dev/null; } expect_pc() { local v; v="$(pc "$1")"; [[ "$v" == "$2" ]] && pass "$1 = $v" || fail "$1 = '$v', expected '$2'"; } -expect_pc relayhost "[smtp.gmail.com]:587" -expect_pc smtp_tls_security_level encrypt -expect_pc smtp_sasl_auth_enable yes -expect_pc smtp_sasl_security_options noanonymous +if [[ "$EXPECT_RELAY" == none ]]; then + v="$(pc relayhost)" + [[ -z "$v" ]] && pass "relayhost unset: no relay (MAIL_RELAY=none), mail stays local" \ + || fail "relayhost = '$v', expected unset (MAIL_RELAY=none)" +else + expect_pc relayhost "[$EXPECT_RELAY_HOST]:$EXPECT_RELAY_PORT" + # TLS as build-templates.sh sets it: required on 587, 465 or with a login. + if [[ "$EXPECT_RELAY_PORT" == 465 ]]; then + expect_pc smtp_tls_security_level encrypt + expect_pc smtp_tls_wrappermode yes + elif [[ "$EXPECT_RELAY_PORT" == 587 ]] || (( RELAY_LOGIN )); then + expect_pc smtp_tls_security_level encrypt + else + expect_pc smtp_tls_security_level may + fi + if (( RELAY_LOGIN )); then + expect_pc smtp_sasl_auth_enable yes + expect_pc smtp_sasl_security_options noanonymous + else + v="$(pc smtp_sasl_auth_enable)" + [[ "$v" != yes ]] && pass "smtp_sasl_auth_enable = ${v:-unset} (no relay login)" \ + || fail "smtp_sasl_auth_enable = yes, but no relay login is expected" + fi +fi expect_pc inet_interfaces loopback-only expect_pc inet_protocols ipv4 expect_pc mydomain "$EXPECT_MAIL_DOMAIN" @@ -261,28 +309,53 @@ expect_pc myhostname "$fqdn" expect_pc myorigin /etc/mailname expect_pc maximal_queue_lifetime 1d expect_pc bounce_queue_lifetime 1d -# No sender rewrite: mail goes out as the natural user@. -for k in sender_canonical_maps smtp_header_checks; do +# Sender rewrite only with MAIL_SENDER; else mail goes out as the natural user@. +unset_keys="sender_canonical_maps smtp_header_checks" why="no sender rewrite" +if [[ -n "$EXPECT_SENDER" ]]; then + expect_pc sender_canonical_maps "static:$EXPECT_SENDER" + expect_pc sender_canonical_classes "envelope_sender, header_sender" + unset_keys="smtp_header_checks" why="no header rewrite" +fi +for k in $unset_keys; do v="$(pc "$k")" - [[ -z "$v" ]] && pass "$k unset (no sender rewrite)" || fail "$k = '$v', expected unset" + [[ -z "$v" ]] && pass "$k unset ($why)" || fail "$k = '$v', expected unset" done -v="$(postconf -n 2>/dev/null | grep -E '^(default|relay)_transport =')" -[[ -z "$v" ]] && pass "default_transport and relay_transport unset (outgoing mail allowed)" \ - || fail "outgoing mail blocked: ${v//$'\n'/; }" -check "root mail aliased (root: line in /etc/aliases)" grep -q '^root:' /etc/aliases -grep -qE "^root:[[:space:]]*${EXPECT_GMAIL_USER}[[:space:]]*$" /etc/aliases 2>/dev/null \ - && pass "root mail goes to $EXPECT_GMAIL_USER" || warn "root: alias in /etc/aliases is not $EXPECT_GMAIL_USER" +if [[ "$EXPECT_RELAY" == none ]]; then + skip "default_transport and relay_transport stay as Local only set them (MAIL_RELAY=none)" +else + v="$(postconf -n 2>/dev/null | grep -E '^(default|relay)_transport =')" + [[ -z "$v" ]] && pass "default_transport and relay_transport unset (outgoing mail allowed)" \ + || fail "outgoing mail blocked: ${v//$'\n'/; }" +fi +if [[ -n "$EXPECT_ROOT_TO" ]]; then + check "root mail aliased (root: line in /etc/aliases)" grep -q '^root:' /etc/aliases + sed -n 's/^root:[[:space:]]*\(.*[^[:space:]]\)[[:space:]]*$/\1/p' /etc/aliases 2>/dev/null | grep -qxF -- "$EXPECT_ROOT_TO" \ + && pass "root mail goes to $EXPECT_ROOT_TO" || warn "root: alias in /etc/aliases is not $EXPECT_ROOT_TO" +else + skip "root mail alias (no MAIL_ROOT_TO)" +fi check 'no cron mail from /etc/crontab (MAILTO="")' grep -qx 'MAILTO=""' /etc/crontab sp=/etc/postfix/sasl/sasl_passwd -if [[ -f "$sp" ]]; then - grep -q "^\[smtp.gmail.com\]:587 ${EXPECT_GMAIL_USER}:." "$sp" && pass "Gmail login set for $EXPECT_GMAIL_USER (password not shown)" \ - || fail "sasl_passwd has no login for $EXPECT_GMAIL_USER" - [[ "$(mode_of "$sp")" == 600 ]] && pass "sasl_passwd is 600" || fail "sasl_passwd mode is $(mode_of "$sp"), expected 600" - ls "$sp".* &>/dev/null && pass "sasl_passwd map built ($(basename "$(ls "$sp".* | head -1)"))" || fail "sasl_passwd map not built (postmap)" -else - fail "$sp missing" +if (( RELAY_LOGIN )); then + if [[ -f "$sp" ]]; then + # The line must start with "[host]:port user:" and have a password after it. + awk -v p="[$EXPECT_RELAY_HOST]:$EXPECT_RELAY_PORT $EXPECT_RELAY_USER:" \ + 'index($0, p) == 1 && length($0) > length(p) { f = 1 } END { exit !f }' "$sp" \ + && pass "$LOGIN_NAME set for $EXPECT_RELAY_USER (password not shown)" \ + || fail "sasl_passwd has no login for $EXPECT_RELAY_USER" + [[ "$(mode_of "$sp")" == 600 ]] && pass "sasl_passwd is 600" || fail "sasl_passwd mode is $(mode_of "$sp"), expected 600" + ls "$sp".* &>/dev/null && pass "sasl_passwd map built ($(basename "$(ls "$sp".* | head -1)"))" || fail "sasl_passwd map not built (postmap)" + else + fail "$sp missing" + fi fi -if [[ -n "$TEST_MAIL" ]]; then +if [[ "$EXPECT_RELAY" == none ]]; then + if [[ -n "$TEST_MAIL" ]]; then + warn "test email not sent to $TEST_MAIL: MAIL_RELAY=none keeps mail on this clone" + else + skip "test email (MAIL_RELAY=none keeps mail on this clone)" + fi +elif [[ -n "$TEST_MAIL" ]]; then echo "Template check from $(hostname) at $(date)" | mail -s "Template check: $(hostname)" "$TEST_MAIL" \ && pass "test email queued to $TEST_MAIL: check your inbox (and: journalctl -u postfix)" \ || fail "could not queue the test email" @@ -298,8 +371,14 @@ check "unattended-upgrades service enabled" systemctl is-enabled --quiet unatten [[ "$(uu_conf APT::Periodic::AutocleanInterval)" == 7 ]] && pass "autoclean every 7 days" || fail "AutocleanInterval is not 7" r="$(uu_conf Unattended-Upgrade::Automatic-Reboot)" [[ "$r" != true ]] && pass "no automatic reboot" || fail "automatic reboot is on (should be off)" -[[ "$(uu_conf Unattended-Upgrade::Mail)" == "$EXPECT_UU_MAIL" ]] && pass "upgrade reports mailed to $EXPECT_UU_MAIL" || fail "Unattended-Upgrade::Mail is not $EXPECT_UU_MAIL" -[[ "$(uu_conf Unattended-Upgrade::MailReport)" == on-change ]] && pass "MailReport on-change" || fail "MailReport is not on-change" +if [[ -n "$EXPECT_UU_MAIL" ]]; then + [[ "$(uu_conf Unattended-Upgrade::Mail)" == "$EXPECT_UU_MAIL" ]] && pass "upgrade reports mailed to $EXPECT_UU_MAIL" || fail "Unattended-Upgrade::Mail is not $EXPECT_UU_MAIL" + [[ "$(uu_conf Unattended-Upgrade::MailReport)" == on-change ]] && pass "MailReport on-change" || fail "MailReport is not on-change" +else + # UU_MAIL blank: the builder writes neither Mail nor MailReport. + v="$(uu_conf Unattended-Upgrade::Mail)" + [[ -z "$v" ]] && pass "no upgrade report mail (UU_MAIL blank)" || fail "Unattended-Upgrade::Mail is '$v', expected unset (UU_MAIL blank)" +fi [[ "$(uu_conf Unattended-Upgrade::Remove-Unused-Dependencies)" == true ]] && pass "Remove-Unused-Dependencies true" || fail "Remove-Unused-Dependencies not true" origins="$(apt-config dump 2>/dev/null)" missing=() @@ -326,26 +405,42 @@ fi if [[ "$LOGIN_USER" == root ]]; then warn "login user is root (Cloud-Init tab): shell, sudo and dotfile checks skipped; set a normal user there to test them" elif [[ -n "$LOGIN_USER" ]]; then - [[ "$(getent passwd "$LOGIN_USER" | cut -d: -f7)" == "$EXPECT_SHELL" ]] \ - && pass "login user $LOGIN_USER uses $EXPECT_SHELL" || fail "login user $LOGIN_USER does not use $EXPECT_SHELL" + if [[ -n "$EXPECT_SHELL" ]]; then + [[ "$(getent passwd "$LOGIN_USER" | cut -d: -f7)" == "$EXPECT_SHELL" ]] \ + && pass "login user $LOGIN_USER uses $EXPECT_SHELL" || fail "login user $LOGIN_USER does not use $EXPECT_SHELL" + else + skip "login user's shell (LOGIN_SHELL blank)" + fi id -nG "$LOGIN_USER" | grep -qw sudo && pass "$LOGIN_USER is in the sudo group" || fail "$LOGIN_USER is not in the sudo group" [[ -s "/home/$LOGIN_USER/.zshrc" ]] && pass "$LOGIN_USER got the dotfiles from /etc/skel" || warn "$LOGIN_USER has no ~/.zshrc" fi -grep -q "^DSHELL=$EXPECT_SHELL" /etc/adduser.conf && pass "adduser default shell $EXPECT_SHELL" || fail "adduser default shell is not $EXPECT_SHELL" -grep -q "^SHELL=$EXPECT_SHELL" /etc/default/useradd && pass "useradd default shell $EXPECT_SHELL" || fail "useradd default shell is not $EXPECT_SHELL" +if [[ -n "$EXPECT_SHELL" ]]; then + grep -q "^DSHELL=$EXPECT_SHELL" /etc/adduser.conf && pass "adduser default shell $EXPECT_SHELL" || fail "adduser default shell is not $EXPECT_SHELL" + grep -q "^SHELL=$EXPECT_SHELL" /etc/default/useradd && pass "useradd default shell $EXPECT_SHELL" || fail "useradd default shell is not $EXPECT_SHELL" +else + skip "adduser and useradd default shell (LOGIN_SHELL blank: OS default)" +fi # ============================================================================= section "needrestart, adverts, journal" -grep -q "\$nrconf{restart} = '$EXPECT_NEEDRESTART';" /etc/needrestart/conf.d/50-restart-mode.conf 2>/dev/null \ - && pass "needrestart restart mode '$EXPECT_NEEDRESTART'" || fail "needrestart restart mode not '$EXPECT_NEEDRESTART'" +if [[ -n "$EXPECT_NEEDRESTART" ]]; then + grep -q "\$nrconf{restart} = '$EXPECT_NEEDRESTART';" /etc/needrestart/conf.d/50-restart-mode.conf 2>/dev/null \ + && pass "needrestart restart mode '$EXPECT_NEEDRESTART'" || fail "needrestart restart mode not '$EXPECT_NEEDRESTART'" +else + skip "needrestart restart mode (NEEDRESTART_MODE blank: OS default)" +fi [[ ! -e /etc/apt/apt.conf.d/20apt-esm-hook.conf ]] && pass "apt Pro/ESM hook disabled" || fail "apt Pro/ESM hook still active" grep -q '^ENABLED=0' /etc/default/motd-news 2>/dev/null && pass "motd news off" || fail "motd news still on" [[ ! -x /etc/update-motd.d/91-contract-ua-esm-status ]] && pass "Pro status removed from the login message" || fail "Pro status still in the login message" -cap="$(sed -n 's/^SystemMaxUse=\([0-9]*\)M$/\1/p' /etc/systemd/journald.conf.d/50-size.conf 2>/dev/null)" -fs_mb=$(( $(df --output=size -k /var/log | tail -1) / 1024 )) -want_cap=$(( fs_mb * EXPECT_JOURNAL_PERCENT / 100 )); (( want_cap > EXPECT_JOURNAL_MAX_MB )) && want_cap=$EXPECT_JOURNAL_MAX_MB -[[ "$cap" == "$want_cap" ]] && pass "journal capped at ${cap}M (min ${EXPECT_JOURNAL_MAX_MB}M, ${EXPECT_JOURNAL_PERCENT}% of ${fs_mb}M)" \ - || fail "journal cap is '${cap:-unset}', expected ${want_cap}M" +if [[ -n "$EXPECT_JOURNAL_MAX_MB" ]]; then + cap="$(sed -n 's/^SystemMaxUse=\([0-9]*\)M$/\1/p' /etc/systemd/journald.conf.d/50-size.conf 2>/dev/null)" + fs_mb=$(( $(df --output=size -k /var/log | tail -1) / 1024 )) + want_cap=$(( fs_mb * EXPECT_JOURNAL_PERCENT / 100 )); (( want_cap > EXPECT_JOURNAL_MAX_MB )) && want_cap=$EXPECT_JOURNAL_MAX_MB + [[ "$cap" == "$want_cap" ]] && pass "journal capped at ${cap}M (min ${EXPECT_JOURNAL_MAX_MB}M, ${EXPECT_JOURNAL_PERCENT}% of ${fs_mb}M)" \ + || fail "journal cap is '${cap:-unset}', expected ${want_cap}M" +else + skip "journal cap (JOURNAL_MAX_MB blank: OS default)" +fi echo " journal now uses: $(journalctl --disk-usage 2>/dev/null | grep -o '[0-9.]*[KMG]' | head -1)" # =============================================================================