Skip to content

Commit 1d0a9ef

Browse files
author
Eric Schoeller
committed
SEPE-1177: Package the receiver alongside the agent
One release tag ships one package containing both binaries and both units. They share the dedup key format -- the agent writes it, the receiver parses it back -- so letting them reach a host at different versions would break acknowledgements in a way neither side could detect. Shipping them together makes that impossible rather than unlikely. The receiver's unit is staged under /var/lib/pdagent/scripts and deliberately not enabled. It cannot start without a webhook signing secret, which only configuration management can place, and a failed start inside a `set -e` postinstall scriptlet would abort before the agent is started -- taking down outbound paging to fix nothing. This repo has already shipped that exact failure once, for a different reason, and the comment explaining it is still in the scriptlet. Enabling the receiver is the deploying system's job, which also gives a dark launch: install everywhere, enable on one host. The package creates the pdagent-receiver account and its config directory at 0750. A separate account is the point of a separate process: the controls that keep an internet-facing listener away from the agent's routing keys -- a targeted ACL on Naemon's command pipe, an nftables egress rule matched on uid, the unit's sandbox -- are all per-user. The archive is now scoped to the agent. The receiver is Linux-only, so including it would give the tarball two binaries on Linux and one on macOS, which goreleaser rejects as confusing. The receiver builds for the same Linux architectures as the agent, including i386. Nobody runs a monitoring host on 32-bit, but a package that claims to be this product and silently lacks one of its two binaries on one architecture is the kind of inconsistency found at install time. The CI layout check caught exactly that while this was being written. That check now asserts both binaries against the ExecStart of their own unit, and that the receiver unit is staged rather than installed. It exists because a bindir default once shipped a package that installed cleanly and then failed to start.
1 parent e0a9922 commit 1d0a9ef

6 files changed

Lines changed: 135 additions & 7 deletions

File tree

‎.github/workflows/build.yml‎

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -92,9 +92,14 @@ jobs:
9292
run: |
9393
set -euo pipefail
9494
unit_path=$(grep -oP '(?<=^ExecStart=)\S+' init/pdagent.service)
95-
echo "Unit invokes: $unit_path"
96-
97-
docker run --rm -v "$PWD/dist:/dist:ro" -e unit_path="$unit_path" \
95+
# The receiver's ExecStart spans continuation lines, so take the
96+
# first field rather than the whole invocation.
97+
recv_path=$(grep -oP '(?<=^ExecStart=)\S+' init/pdagent-receiver.service)
98+
echo "Agent unit invokes: $unit_path"
99+
echo "Receiver unit invokes: $recv_path"
100+
101+
docker run --rm -v "$PWD/dist:/dist:ro" \
102+
-e unit_path="$unit_path" -e recv_path="$recv_path" \
98103
rockylinux:8 bash -euo pipefail -c '
99104
shopt -s nullglob
100105
rpms=(/dist/*.rpm)
@@ -110,13 +115,25 @@ jobs:
110115
files=$(rpm -qpl "$rpm")
111116
echo "$files"
112117
113-
for f in "$unit_path" /usr/local/bin/pd-send /usr/local/bin/pd-queue \
114-
/var/lib/pdagent/scripts/pdagent.service; do
118+
for f in "$unit_path" "$recv_path" \
119+
/usr/local/bin/pd-send /usr/local/bin/pd-queue \
120+
/var/lib/pdagent/scripts/pdagent.service \
121+
/var/lib/pdagent/scripts/pdagent-receiver.service; do
115122
if ! grep -qx "$f" <<<"$files"; then
116123
echo "::error::$rpm is missing expected path $f"
117124
exit 1
118125
fi
119126
done
127+
128+
# The receiver unit must ship staged, not installed. Dropping it
129+
# straight into /lib/systemd/system would present a unit that
130+
# cannot start until configuration management supplies a signing
131+
# secret, and a failed start there would abort the postinstall
132+
# scriptlet before the paging daemon is started.
133+
if grep -qx "/lib/systemd/system/pdagent-receiver.service" <<<"$files"; then
134+
echo "::error::$rpm installs the receiver unit directly; it must be staged under /var/lib/pdagent/scripts"
135+
exit 1
136+
fi
120137
done
121138
echo "Package layout OK across ${#rpms[@]} RPM(s)"
122139
'

‎.goreleaser.yml‎

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ before:
66
- go generate ./...
77

88
builds:
9-
- binary: pdagent
9+
- id: pdagent
10+
binary: pdagent
1011
env:
1112
- CGO_ENABLED=0
1213
# Matches the platform set the project has historically released.
@@ -26,10 +27,45 @@ builds:
2627
- -X 'github.com/PagerDuty/go-pdagent/pkg/common.Commit={{.ShortCommit}}'
2728
- -X 'github.com/PagerDuty/go-pdagent/pkg/common.Date={{.Date}}'
2829

30+
# The inbound webhook receiver. Built from the same module and shipped in the
31+
# same package as the agent, so the two can never disagree about the dedup key
32+
# format they share -- one is the writer and the other the reader, and a
33+
# version skew between them would break acknowledgements silently.
34+
#
35+
# Linux only. It exists to write Naemon's command pipe on a monitoring host,
36+
# which is not a thing anyone does on a Mac.
37+
- id: pdagent-receiver
38+
binary: pdagent-receiver
39+
main: ./cmd/receiver
40+
env:
41+
- CGO_ENABLED=0
42+
goos:
43+
- linux
44+
# The same Linux architectures the agent ships. Not because anyone runs a
45+
# monitoring host on i386, but because a package that claims to be this
46+
# product and silently lacks one of its two binaries on one architecture is
47+
# the kind of inconsistency that is discovered at install time.
48+
goarch:
49+
- amd64
50+
- arm64
51+
- "386"
52+
ldflags:
53+
- -s -w
54+
- -X 'github.com/PagerDuty/go-pdagent/pkg/common.Version={{.Version}}'
55+
- -X 'github.com/PagerDuty/go-pdagent/pkg/common.Commit={{.ShortCommit}}'
56+
- -X 'github.com/PagerDuty/go-pdagent/pkg/common.Date={{.Date}}'
57+
2958
# `replacements` was removed in goreleaser v2; the same artifact names are
3059
# now produced by templating the OS and architecture directly.
60+
#
61+
# Scoped to the agent alone. The receiver is a Linux-only server component
62+
# deployed from the rpm or deb, so including it here would make the archive
63+
# hold two binaries on Linux and one on macOS -- which goreleaser rejects, and
64+
# rightly, as confusing to anyone downloading it.
3165
archives:
32-
- name_template: >-
66+
- ids:
67+
- pdagent
68+
name_template: >-
3369
{{ .ProjectName }}_{{ .Version }}_
3470
{{- title .Os }}_
3571
{{- if eq .Arch "amd64" }}x86_64
@@ -74,8 +110,19 @@ nfpms:
74110
dst: "/var/lib/pdagent/scripts/pdagent.init"
75111
- src: "init/pdagent.service"
76112
dst: "/var/lib/pdagent/scripts/pdagent.service"
113+
# Staged, not installed into /lib/systemd/system by the package. The
114+
# receiver needs a signing secret the package cannot supply, so it is
115+
# enabled by configuration management once that is in place. This also
116+
# keeps a receiver that cannot start from aborting the postinstall
117+
# scriptlet and leaving the paging daemon down.
118+
- src: "init/pdagent-receiver.service"
119+
dst: "/var/lib/pdagent/scripts/pdagent-receiver.service"
77120
- src: "scripts/pd-*"
78121
dst: "/usr/local/bin/"
122+
- dst: /etc/pdagent-receiver
123+
type: dir
124+
file_info:
125+
mode: 0750
79126
# `empty_folders` was removed in goreleaser v2; directories the agent
80127
# needs at runtime are now declared as dir-type contents so the package
81128
# still owns them and the postinstall chown has something to act on.

‎scripts/deb/postinstall.sh‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,17 @@ install_init () {
1111

1212
install_systemd () {
1313
cp /var/lib/pdagent/scripts/pdagent.service /lib/systemd/system
14+
15+
# The receiver's unit is installed but deliberately neither enabled nor
16+
# started. It requires a webhook signing secret that only configuration
17+
# management can place, so starting it here would fail -- and a failure
18+
# under `set -e` would abort this script before the agent below is started,
19+
# taking down outbound paging to fix nothing.
20+
if [ -f /var/lib/pdagent/scripts/pdagent-receiver.service ]; then
21+
cp /var/lib/pdagent/scripts/pdagent-receiver.service /lib/systemd/system
22+
fi
23+
24+
systemctl daemon-reload || :
1425
systemctl enable pdagent
1526
systemctl start pdagent
1627
}
@@ -21,6 +32,17 @@ if [ "$1" = "configure" ]; then
2132
/usr/sbin/adduser --system --shell /bin/false --no-create-home \
2233
--group pdagent
2334

35+
# The receiver runs as its own account. It listens to the public internet
36+
# and needs write access to Naemon's command pipe, while the agent holds
37+
# PagerDuty routing keys -- a compromise of either should not reach the
38+
# other, and every control that enforces that is per-user.
39+
/usr/bin/getent passwd pdagent-receiver >/dev/null || \
40+
/usr/sbin/adduser --system --shell /bin/false --no-create-home \
41+
--group pdagent-receiver
42+
43+
chown pdagent-receiver:pdagent-receiver /etc/pdagent-receiver 2>/dev/null || :
44+
chmod 0750 /etc/pdagent-receiver 2>/dev/null || :
45+
2446
# `pdagent init` refuses to overwrite an existing config and exits
2547
# non-zero. The package manager preserves /etc/pdagent/config.yaml across
2648
# removal and upgrade, so on any reinstall or upgrade this aborted the

‎scripts/deb/preremove.sh‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,15 @@ uninstall_init () {
88
}
99

1010
uninstall_systemd () {
11+
# Stop the receiver first. It may hold Naemon's command pipe open, and
12+
# leaving an enabled unit behind for a binary that is being removed would
13+
# make the next boot log a failure for something nobody installed.
14+
if systemctl list-unit-files pdagent-receiver.service >/dev/null 2>&1; then
15+
systemctl stop pdagent-receiver || :
16+
systemctl disable pdagent-receiver || :
17+
fi
18+
rm -f /lib/systemd/system/pdagent-receiver.service
19+
1120
systemctl stop pdagent
1221
systemctl disable pdagent
1322
}

‎scripts/rpm/postinstall.sh‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,18 @@ install_init () {
1111

1212
install_systemd () {
1313
cp /var/lib/pdagent/scripts/pdagent.service /lib/systemd/system
14+
15+
# The receiver's unit is installed but deliberately neither enabled nor
16+
# started. It requires a webhook signing secret that only configuration
17+
# management can place, so starting it here would fail -- and a failure
18+
# under `set -e` would abort this scriptlet before the agent below is
19+
# started, taking down outbound paging to fix nothing. Enabling it is the
20+
# deploying system's job, once the secret exists.
21+
if [ -f /var/lib/pdagent/scripts/pdagent-receiver.service ]; then
22+
cp /var/lib/pdagent/scripts/pdagent-receiver.service /lib/systemd/system
23+
fi
24+
25+
systemctl daemon-reload || :
1426
systemctl enable pdagent
1527
systemctl start pdagent
1628
}
@@ -19,6 +31,18 @@ install_systemd () {
1931
/usr/bin/getent passwd pdagent >/dev/null || \
2032
/usr/sbin/adduser --system --shell /bin/false --no-create-home pdagent
2133

34+
# The receiver runs as its own account. It listens to the public internet and
35+
# needs write access to Naemon's command pipe, while the agent holds PagerDuty
36+
# routing keys -- a compromise of either should not reach the other, and every
37+
# control that enforces that (a targeted ACL, an nftables egress rule matched on
38+
# uid, the unit's sandbox) is per-user.
39+
/usr/bin/getent passwd pdagent-receiver >/dev/null || \
40+
/usr/sbin/adduser --system --shell /bin/false --no-create-home pdagent-receiver
41+
42+
# Only the receiver's own account may read its signing secret.
43+
chown pdagent-receiver:pdagent-receiver /etc/pdagent-receiver 2>/dev/null || :
44+
chmod 0750 /etc/pdagent-receiver 2>/dev/null || :
45+
2246
# `pdagent init` refuses to overwrite an existing config and exits non-zero.
2347
# rpm preserves /etc/pdagent/config.yaml across erase and upgrade, so on any
2448
# reinstall or upgrade this aborted the scriptlet under `set -e` -- before the

‎scripts/rpm/preremove.sh‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,15 @@ uninstall_init () {
88
}
99

1010
uninstall_systemd () {
11+
# Stop the receiver first. It may hold Naemon's command pipe open, and
12+
# leaving an enabled unit behind for a binary that is being removed would
13+
# make the next boot log a failure for something nobody installed.
14+
if systemctl list-unit-files pdagent-receiver.service >/dev/null 2>&1; then
15+
systemctl stop pdagent-receiver || :
16+
systemctl disable pdagent-receiver || :
17+
fi
18+
rm -f /lib/systemd/system/pdagent-receiver.service
19+
1120
systemctl stop pdagent
1221
systemctl disable pdagent
1322
}

0 commit comments

Comments
 (0)