carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1#!/bin/bash
2# đŸ¤– First-boot init for a CARLOS scanner host (Debian arm64). Rendered by
3# infra/modules/scanner-host. Each step says where it got to on the serial
4# console, so `aws ec2 get-console-output` answers "how far did it get".
5#
6# The order is the design:
7# 1. SSM agent, because with no SSH it is the only way in to fix the rest.
8# 2. Packages installed but NOT started.
9# 3. Config, the self-test and the gate units.
10# 4. First signatures, alone, before anything else touches the database.
11# 5. Start: clamd, then the self-test, then (only on a pass) the port.
12# 6. Verify from the outside of the gate, and say READY or FAILED.
13set -euo pipefail
14
15# Logging must never be what stops the script: a failed write inside say()
16# would exit under set -e without the ERR trap (functions do not inherit it),
17# so without a FAILED marker. stdout goes to cloud-init's log; the serial
18# console copy is best effort.
19say() {
20 echo "scanner-init: $*" || true
21 { echo "scanner-init: $*" > /dev/console; } 2>/dev/null || true
22}
23# A failure in step 2 could leave policy-rc.d behind, and then nobody fixing
24# the box over SSM could start a service. Remove it on every way out.
25fail() { rm -f /usr/sbin/policy-rc.d || true; say "SCANNER-HOST FAILED: $*"; exit 1; }
26trap 'fail "line $LINENO"' ERR
27export DEBIAN_FRONTEND=noninteractive
28
29# ---- 1. the way in ---------------------------------------------------------
30say "1/6 SSM agent"
31apt-get update -q
32apt-get install -qy curl unattended-upgrades
33curl -fsSL -o /var/tmp/amazon-ssm-agent.deb \
34 "https://s3.${region}.amazonaws.com/amazon-ssm-${region}/latest/debian_arm64/amazon-ssm-agent.deb"
35dpkg -i /var/tmp/amazon-ssm-agent.deb
36rm -f /var/tmp/amazon-ssm-agent.deb
37systemctl enable --now amazon-ssm-agent
38systemctl enable --now unattended-upgrades
39
40# ---- 2. packages, not started ----------------------------------------------
41# Debian's postinst starts clamav-freshclam and the daemon. Neither may run
42# before step 3 has configured it, and freshclam's service would fight the
43# foreground run in step 4 for the database lock.
44say "2/6 clamav packages"
45printf '#!/bin/sh\nexit 101\n' > /usr/sbin/policy-rc.d
46chmod +x /usr/sbin/policy-rc.d
47apt-get install -qy clamav-daemon clamav-freshclam clamdscan python3 iproute2
48rm -f /usr/sbin/policy-rc.d
49
50# ---- 3. config, self-test, gate --------------------------------------------
51say "3/6 config and gate"
52conf=/etc/clamav/clamd.conf
53[ -f "$conf" ] || fail "the package wrote no $conf"
54# With debconf on (the package default), the postinst regenerates clamd.conf
55# on every configure, unattended upgrades included. The file it generated at
56# install is the base; from here on it is ours. With debconf off, the postinst
57# leaves an existing clamd.conf alone.
58echo 'clamav-daemon clamav-daemon/debconf boolean false' | debconf-set-selections
59# clamd has no include directory: replace each key in place. The ERR trap is
60# not inherited by functions, so a failure in here reports itself.
61setkey() {
62 { sed -i "/^$1[[:space:]]/d" "$conf" && echo "$1 $2" >> "$conf"; } \
63 || fail "could not set $1 in $conf"
64}
65# In M, never bytes: if the postinst ever does regenerate this file, it reads
66# the number as megabytes and appends M (26214400 would become 25 TiB).
67setkey StreamMaxLength ${stream_max_mib}M
68setkey AlertExceedsMax yes
69setkey MaxFileSize ${scan_limit}
70setkey MaxScanSize ${scan_limit}
71setkey ConcurrentDatabaseReload ${concurrent_reload}
72# Debian 13 mounts /tmp as tmpfs: extraction of up to the scan limit per scan
73# belongs on disk, not in memory.
74setkey TemporaryDirectory /var/tmp
75# clamd itself never listens on TCP. The gated proxy does.
76sed -i '/^TCPSocket[[:space:]]/d;/^TCPAddr[[:space:]]/d' "$conf"
77
78# Debian's AppArmor profile for clamd is enforced and allows temp files under
79# /tmp only, and clamd writes every INSTREAM to a temp file. Without this,
80# every scan errors and the self-test fails. The profile includes this local
81# file. It is loaded here, before clamd first starts.
82if [ -e /etc/apparmor.d/usr.sbin.clamd ]; then
83 mkdir -p /etc/apparmor.d/local
84 cat > /etc/apparmor.d/local/usr.sbin.clamd <<'EOF'
85# scanner-host: clamd's TemporaryDirectory is /var/tmp.
86/var/tmp/ rw,
87/var/tmp/** krw,
88EOF
89 if command -v apparmor_parser >/dev/null && [ -d /sys/kernel/security/apparmor ]; then
90 apparmor_parser -r /etc/apparmor.d/usr.sbin.clamd
91 fi
92fi
93
94echo '${selftest_b64}' | base64 -d > /usr/local/sbin/scanner-selftest
95chmod 0755 /usr/local/sbin/scanner-selftest
96echo '${selftest_unit_b64}' | base64 -d > /etc/systemd/system/scanner-selftest.service
97echo '${tcp_service_b64}' | base64 -d > /etc/systemd/system/scanner-tcp.service
98
99tok=$(curl -fsS -X PUT http://169.254.169.254/latest/api/token \
100 -H 'X-aws-ec2-metadata-token-ttl-seconds: 300')
101ip=$(curl -fsS -H "X-aws-ec2-metadata-token: $tok" \
102 http://169.254.169.254/latest/meta-data/local-ipv4)
103case $ip in
104 [0-9]*.[0-9]*.[0-9]*.[0-9]*) ;;
105 *) fail "no private IPv4 from instance metadata: '$ip'" ;;
106esac
107echo '${tcp_socket_b64}' | base64 -d | sed "s/@PRIVATE_IP@/$ip/" \
108 > /etc/systemd/system/scanner-tcp.socket
109grep -q "^ListenStream=$ip:3310\$" /etc/systemd/system/scanner-tcp.socket \
110 || fail "the listener is not bound to the private address"
111
112mkdir -p /etc/systemd/system/clamav-daemon.service.d
113cat > /etc/systemd/system/clamav-daemon.service.d/10-scanner.conf <<EOF
114# Every start of clamd, an automatic restart included, asks for the gate again.
115[Unit]
116Wants=scanner-tcp.socket
117
118# The kernel kills clamd under memory pressure, never the SSM agent.
119[Service]
120MemoryMax=${memory_max}
121MemoryHigh=${memory_high}
122Restart=on-failure
123
124# The same two lines as Debian's extend.conf drop-in, which the postinst moves
125# aside on its first configure run with debconf off: an upgrade or a
126# dpkg-reconfigure, whichever comes first.
127ExecStartPre=-/bin/mkdir -p /run/clamav
128ExecStartPre=/bin/chown clamav /run/clamav
129EOF
130systemctl daemon-reload
131
132# ---- 4. first signatures ---------------------------------------------------
133say "4/6 first signatures"
134# The packaged config, unchanged. Debian's AppArmor profile confines freshclam
135# to its config under /etc/clamav, so a copy anywhere else cannot be read. With
136# clamd not running yet, the NotifyClamd step only warns, and freshclam still
137# exits 0.
138# ClamAV's mirror often rate-limits a new address. Three tries, a minute apart.
139tries=1
140until freshclam; do
141 [ $tries -lt 3 ] || fail "first freshclam failed 3 times, 60s apart (rate-limited by ClamAV's mirror? an AppArmor denial shows in: journalctl -k | grep freshclam)"
142 say "freshclam try $tries failed, retrying in 60s"
143 tries=$((tries + 1))
144 sleep 60
145done
146for db in main daily bytecode; do
147 ls /var/lib/clamav/$db.c[vl]d >/dev/null 2>&1 || fail "no $db signature database after freshclam"
148done
149systemctl enable --now clamav-freshclam
150
151# ---- 5. start, in the order the units enforce ------------------------------
152say "5/6 clamd, self-test, listener (several minutes)"
153systemctl enable clamav-daemon.service scanner-selftest.service scanner-tcp.socket
154systemctl start clamav-daemon.socket clamav-daemon.service
155# Blocks until the self-test has finished. Fails if it failed.
156systemctl start scanner-tcp.socket \
157 || fail "self-test or listener did not start: $(systemctl is-active clamav-daemon.service scanner-selftest.service scanner-tcp.socket | tr '\n' ' ')"
158
159# ---- 6. verify from outside the gate ---------------------------------------
160say "6/6 verify"
161[ "$(systemctl is-active scanner-selftest.service)" = active ] || fail "self-test is not active"
162[ "$(systemctl is-active scanner-tcp.socket)" = active ] || fail "listener is not active"
163bound=$(ss -ltnH 'sport = :3310' | awk '{print $4}' | sort -u | tr '\n' ' ')
164[ "$bound" = "$ip:3310 " ] || fail "3310 is bound to '$bound', want only $ip:3310"
165exec 3<>"/dev/tcp/$ip/3310"
166printf 'zVERSION\0' >&3
167ver=$(timeout 20 head -c 200 <&3 | tr -d '\0')
168exec 3>&-
169case $ver in
170 ClamAV*) ;;
171 *) fail "no VERSION answer through the listener: '$ver'" ;;
172esac
173mkdir -p /var/lib/scanner-host
174echo "$ver" > /var/lib/scanner-host/ready
175say "SCANNER-HOST READY $ver"
176