Repository navigation
Expand file tree
/
Copy pathinstall-server-python.sh
More file actions
executable file
·281 lines (254 loc) · 12 KB
/
Copy pathinstall-server-python.sh
File metadata and controls
executable file
·281 lines (254 loc) · 12 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
#!/usr/bin/env bash
#
# Installs PFUI Firewall
#
err=0
trap 'err=1; echo "$(basename "$0"): failed at line $LINENO: $BASH_COMMAND" >&2' ERR
# Abort immediately on a step nothing can proceed without
die() {
echo "PFUIFW: FATAL: $*" >&2
exit 1
}
# Install packages without ever prompting. A release's package set holds one
# version of each package, so there is no "latest" to choose; -i only ever
# prompts when a stem matches several flavours, and answering that from a
# script is guesswork. Name the candidates instead and let the operator pick.
add_pkg() {
local pkg
for pkg in "$@"; do
pkg_add -I "${pkg}" && continue
echo "PFUIFW: cannot install '${pkg}'. Candidates:" >&2
pkg_info -Q "${pkg}" >&2 || true
die "install one explicitly, Eg 'pkg_add ${pkg}-<version>'"
done
}
# install(1) renames into place, so a binary replaced under a running daemon
# leaves that daemon on the old inode.
SERVICE_WAS_RUNNING=no
stop_for_upgrade() {
rcctl check pfui_firewall >/dev/null 2>&1 || return 0
SERVICE_WAS_RUNNING=yes
echo "PFUIFW: Stopping pfui_firewall to upgrade it"
rcctl stop pfui_firewall >/dev/null 2>&1 \
|| die "cannot stop pfui_firewall; stop it by hand and re-run"
}
# Only what was running is started. A failure is loud: the firewall was
# whitelisting a moment ago.
start_after_upgrade() {
[ "${SERVICE_WAS_RUNNING}" = yes ] || return 0
echo "PFUIFW: Starting pfui_firewall"
if rcctl start pfui_firewall; then
return 0
fi
err=1
echo "PFUIFW: FAILED to start pfui_firewall, which was running before this" >&2
echo " upgrade, so this firewall is not whitelisting now. Its reason:" >&2
echo " tail /var/log/daemon | grep pfui_firewall" >&2
}
# Ask the running Redis what it is bound to. Retried, because rcctl returns once
# the process exists, which precedes it accepting connections. Redis holds the
# whitelist the sync loop pushes into PF, so anything that reaches it can
# authorise egress.
report_redis_bind() {
local bind="" token tries=0
while [ "${tries}" -lt 10 ]; do
bind=$(redis-cli config get bind 2>/dev/null | tail -1)
[ -n "${bind}" ] && break
tries=$((tries + 1))
sleep 1
done
# Unverifiable is not the same as wrong, so this warns rather than failing
if [ -z "${bind}" ]; then
echo "PFUIFW: WARNING Redis did not answer, so its bind address was not verified."
echo " Check it listens on loopback only: redis-cli config get bind"
return 0
fi
echo "PFUIFW: Redis is bound to: ${bind}"
# Redis writes this as a list, and '-' marks an address it may skip
for token in ${bind}; do
case "${token#-}" in
127.*|::1|localhost) ;;
*)
err=1
echo "PFUIFW: WARNING Redis is bound to '${bind}', which is not loopback only." >&2
echo " Anything that reaches it can authorise egress through PF." >&2
echo " Set 'bind 127.0.0.1' in ${REDIS_CONF} and restart redis." >&2
return 0
;;
esac
done
}
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
HOUR=$(date +%d-%b-%H_%M)
if [[ $(id -u) -ne 0 ]]; then
echo "PFUIFW: Please run as root user"
exit 1
fi
echo "***************************************************************************************"
echo "* Welcome to the PFUI_Firewall (Python) Installer (https://github.com/andylemin/PFUI) *"
echo "***************************************************************************************"
if [[ $(uname) == "OpenBSD" ]]; then
OS="OpenBSD"
else
echo "PFUIFW: Looks like a non-supported operating system."
echo "PFUIFW Supported OS: OpenBSD"
exit 2
fi
if [[ "$OS" = "OpenBSD" ]]; then
echo "PFUIFW: Installing Python3"
# No python3 stem exists, only python-3.x packages with a branch each; the
# py3-* packages depend on the right one and bring it with them
add_pkg py3-pip py3-setuptools
command -v python3 >/dev/null \
|| die "python3 is absent; name a branch explicitly, Eg 'pkg_add python%3.13'"
which python >/dev/null
if [[ $? != 0 ]]; then
ln -s `which python3` /usr/local/bin/python
fi
echo "PFUIFW: Installing and Starting Redis"
add_pkg redis
# Redis holds the whitelist that the sync loop pushes into the PF tables, so
# anything able to write those keys can authorise egress. Keep it loopback-only.
REDIS_CONF=/etc/redis/redis.conf
REDIS_CONF_CHANGED=no
if [ -f "${REDIS_CONF}" ]; then
cp -p "${REDIS_CONF}" "${REDIS_CONF}.${HOUR}"
if grep -Eq '^[[:space:]]*bind[[:space:]]' "${REDIS_CONF}"; then
sed -i -e 's/^[[:space:]]*bind[[:space:]].*/bind 127.0.0.1/' "${REDIS_CONF}"
else
echo "bind 127.0.0.1" >> "${REDIS_CONF}"
fi
grep -Eq '^[[:space:]]*protected-mode' "${REDIS_CONF}" \
|| echo "protected-mode yes" >> "${REDIS_CONF}"
if cmp -s "${REDIS_CONF}" "${REDIS_CONF}.${HOUR}"; then
rm -f "${REDIS_CONF}.${HOUR}"
else
REDIS_CONF_CHANGED=yes
echo "PFUIFW: ${REDIS_CONF} updated (backup at ${REDIS_CONF}.${HOUR})"
fi
else
echo "PFUIFW: WARNING ${REDIS_CONF} not found; verify Redis binds 127.0.0.1 only"
fi
# Only sets status, so a configured rtable, user or flags are untouched
rcctl enable redis
# Restarting drops every connection the daemon holds, so it happens only when
# this changed the configuration or Redis is not running
if [ "${REDIS_CONF_CHANGED}" = yes ] || ! rcctl check redis >/dev/null 2>&1; then
rcctl restart redis
else
echo "PFUIFW: Redis already running and its configuration unchanged; left alone"
fi
report_redis_bind
echo "PFUIFW: Creating daemon user '_pfui_firewall'"
groupadd _pfui_firewall 2>/dev/null || true
useradd -g _pfui_firewall -s /sbin/nologin -d /var/empty _pfui_firewall 2>/dev/null || true
id _pfui_firewall >/dev/null || die "daemon user _pfui_firewall was not created"
# Shared group for the local socket (SOCKET_UNIX), which a PFUI_Unbound on this
# same host connects to. It is a group of its own rather than reusing _unbound
# or _pfui_firewall: membership of it means "may inject PF whitelist entries",
# and that should not be implied by running as the resolver or as the daemon.
# install-client-unbound.sh adds _unbound to it when a resolver is installed here.
echo "PFUIFW: Creating group '_pfui' (permits the local PFUI socket)"
groupadd _pfui 2>/dev/null || true
groupinfo _pfui >/dev/null 2>&1 || die "group _pfui was not created"
# -G replaces secondary memberships on OpenBSD, so it is only applied when the
# daemon account is not already in the group. _pfui_firewall is created here and
# has no other secondary groups
if ! groupinfo _pfui | grep -qw _pfui_firewall; then
usermod -G _pfui _pfui_firewall || die "cannot add _pfui_firewall to _pfui"
fi
# PF ioctl access without wheel, which would also grant su. rc.d/pfui_firewall
# re-applies this on every start, because MAKEDEV resets it on release upgrades
chgrp _pfui_firewall /dev/pf && chmod 660 /dev/pf || die "cannot set /dev/pf ownership"
echo "PFUIFW: Installing Python dependencies for the system interpreter"
# Not a virtualenv: the daemon runs under /usr/local/bin/python3 (see its
# shebang), so the dependencies have to be importable there.
add_pkg py3-lz4 py3-yaml py3-redis
# 'service' is not packaged, and the system interpreter is externally managed
# (PEP 668), so pip has to be told that writing to it is deliberate
python3 -m pip install --break-system-packages 'service>=0.6,<1' \
|| die "cannot install the 'service' module"
python3 -c "import lz4.frame, yaml, redis, service" \
|| die "dependencies are not importable by $(command -v python3)"
stop_for_upgrade
echo "PFUIFW: Installing PFUI Firewall Service (will backup any existing pfui_firewall configuration)"
install -m 755 -o root -g wheel "${DIR}"/server-python/pfui_firewall.py /usr/local/sbin/pfui_firewall \
|| die "cannot install the daemon"
# Shared modules must sit beside the daemon; a script's own directory is sys.path[0]
install -d -m 755 -o root -g wheel /usr/local/sbin/pfui
# Shared protocol module sits beside the daemon, as a top-level module
install -m 644 -o root -g wheel "${DIR}"/protocol/python/pfui_wire.py \
/usr/local/sbin/ || die "cannot install the shared wire module"
install -m 644 -o root -g wheel "${DIR}"/server-python/pfui/__init__.py \
"${DIR}"/server-python/pfui/store.py "${DIR}"/server-python/pfui/validate.py \
"${DIR}"/server-python/pfui/pf_ioctl.py /usr/local/sbin/pfui/ \
|| die "cannot install the pfui modules"
# An existing config is kept so an upgrade does not disturb a running
# deployment; only a first install lays down the example
if [ -f /etc/pfui_firewall.yml ]; then
cp -p /etc/pfui_firewall.yml "/etc/pfui_firewall.yml.${HOUR}"
echo "PFUIFW: Keeping the existing /etc/pfui_firewall.yml"
echo "PFUIFW: (backup at /etc/pfui_firewall.yml.${HOUR}; compare it against"
echo " ${DIR}/server-python/pfui_firewall.yml for keys added since)"
else
# root-owned: the daemon only reads this, and CTL: PFCTL makes it a command source
install -m 644 -o root -g wheel "${DIR}"/server-python/pfui_firewall.yml \
/etc/pfui_firewall.yml || die "cannot install the configuration file"
echo "PFUIFW: Default configuration installed at '/etc/pfui_firewall.yml' (please configure)"
fi
# root-owned: rcctl runs this as root, and a file's owner can always chmod it
install -m 555 -o root -g wheel "${DIR}"/server-python/rc.d/pfui_firewall /etc/rc.d/pfui_firewall \
|| die "cannot install the rc.d script"
install -m 644 -o root -g wheel "${DIR}"/examples/pf.conf /etc/pf-pfui-example.conf
echo "PFUIFW: An example pf.conf file is located at '/etc/pf-pfui-example.conf'"
echo "PFUIFW: /etc/pf.conf is NOT modified; merge the PFUI tables and rules yourself"
# Where the daemon's pid file and, if SOCKET_UNIX is configured, its local socket
# live. Group _pfui at 0750 so only that group can traverse to the socket, and so
# the socket inherits the group; rc.d/pfui_firewall re-applies this on every start
install -d -o _pfui_firewall -g _pfui -m 750 /var/run/pfui \
|| die "cannot create /var/run/pfui"
PERSIST_DIR=/var/db/pfui
echo "PFUIFW: Persist files in ${PERSIST_DIR}"
# Daemon-owned directory: file_push/file_pop need to create a .lock sidecar and
# rename a tempfile over the persist file, so the directory itself must be
# writable, not just the files. pfctl reads them as root, so no world access.
install -d -o _pfui_firewall -g _pfui_firewall -m 750 "${PERSIST_DIR}" \
|| die "cannot create ${PERSIST_DIR}"
for af in ipv4 ipv6; do
new="${PERSIST_DIR}/${af}_domains"
# An existing file from an earlier layout is COPIED, never moved: pf.conf
# loads its tables from whatever path it names, and moving the file out from
# under it breaks the next ruleset load
if [ ! -f "${new}" ]; then
for old in "/var/spool/pfui/pfui_${af}_domains" "/var/spool/pfui_${af}_domains"; do
if [ -f "${old}" ]; then
echo "PFUIFW: Seeding ${new} from ${old} (${old} is left in place)"
cp "${old}" "${new}"
break
fi
done
fi
[ -f "${new}" ] || install -o _pfui_firewall -g _pfui_firewall -m 640 /dev/null "${new}"
chown _pfui_firewall:_pfui_firewall "${new}"
chmod 640 "${new}"
done
echo "PFUIFW: NOTE point pf.conf 'persist file' paths at ${PERSIST_DIR}/ipv{4,6}_domains (see /etc/pf-pfui-example.conf)"
fi
start_after_upgrade
if [[ $err != 0 ]]; then
echo "PFUIFW: All Completed, but with some errors. Please investigate."
exit 1
else
echo "PFUIFW: All Completed successfully."
fi
if [ "${SERVICE_WAS_RUNNING}" = yes ]; then
echo "PFUIFW: pfui_firewall is running the version just installed"
else
echo "PFUIFW: Enable service 'rcctl enable pfui_firewall'"
echo "PFUIFW: Start service 'rcctl start pfui_firewall'"
fi
echo
echo "PFUIFW: If PFUI_Unbound runs on THIS host, uncomment SOCKET_UNIX in"
echo " /etc/pfui_firewall.yml and add '- SOCKET: /var/run/pfui/pfui_firewall.sock'"
echo " to the resolver's FIREWALLS. No pf.conf rule is needed for that path;"
echo " membership of group '_pfui' is what permits it."