Skip to content

SEPE-1165: Fix defects inherited from the upstream beta - #11

Merged
eschoeller merged 2 commits into
mainfrom
SEPE-1165_inherited_defects
Aug 4, 2026
Merged

SEPE-1165: Fix defects inherited from the upstream beta#11
eschoeller merged 2 commits into
mainfrom
SEPE-1165_inherited_defects

Conversation

@eschoeller

Copy link
Copy Markdown
Contributor

Six defects surfaced by four independent analyses during the SEPE-1177 design work. All predate this fork — pkg/server, pkg/common and cmd are byte-identical to upstream — and all sit in the path that delivers pages.

Defect Consequence
No Restart= in the unit Any crash left the agent dead until a human noticed. RestartSec was set, which reads as though restarts were configured, but it is inert alone.
Auth middleware allowed everything when the secret was empty An unauthenticated enqueue endpoint. Enqueued events page people.
Byte-by-byte token comparison Leaks the shared secret to anyone who can time the response.
Presented credential logged on every auth failure Into a world-readable log.
math/rand seeded from the wall clock for the auth secret The keyspace is irrelevant when the entropy is one timestamp recoverable from a file mtime.
req.Header["Pd-Event-Version"][0] Panics on a missing header. Recovered per-connection, so the process survived, but trivially reachable.
Request body logged at debug Includes the routing key, which permits resolving arbitrary incidents.

The one that needed care

Config permissions were not a chmod. viper writes 0644, and the file holds the daemon's auth secret — but the CLI runs as the monitoring user and reaches it only through that world-read bit. Removing it naively would leave the CLI unable to enqueue, which stops paging silently — the exact failure mode this PR fixes elsewhere.

Group ownership now carries the access where a monitoring user exists, falling back to owner-only where one does not. Verified on RHEL 8:

after:  pdagent:naemon 640
naemon can read:  yes
pdagent can read: yes
apache can read:  yes
CLI health check: OK

Residual, stated deliberately: apache is in the naemon group on these hosts, so it can still read the file. Acceptable — it already holds write access to Naemon's command pipe, which is strictly more power than a routing key.

Notes

  • StartLimit* are in [Unit]. In [Service] they are silently ignored on systemd 239 as shipped by RHEL 8, leaving a unit that looks rate-limited and is not.
  • Refusing when no secret is configured is a behaviour change, but that state cannot arise by accident: the defaults generate a key and pdagent init writes it. It now says why, since the symptom is otherwise indistinguishable from a wrong token.
  • pkg/server had no tests. These are the first, covering the two behaviour changes.
  • Deliberately not included: the unbuffered-channel deadlock in pkg/persistentqueue/enqueue.go and the unbounded shutdown drain in pkg/eventqueue. Both are real and documented, but they change delivery semantics and cannot be validated end to end without PagerDuty access, which has lapsed. Fixing the paging queue without being able to run a real delivery is the wrong trade.

Six defects, all present before this fork and all in the path that
delivers pages. pkg/server, pkg/common and cmd are byte-identical to
upstream, so these came with the beta rather than from our changes.

The systemd unit had no Restart=. RestartSec was set, which reads as
though restarts were configured, but it is inert on its own and the
default is Restart=no -- so any crash left the agent dead until a human
noticed by other means. StartLimit* go in [Unit], because [Service] is
silently ignored on systemd 239 as shipped by RHEL 8.

The auth middleware allowed every request through when no secret was
configured. That state cannot arise by accident, since the defaults
generate a key and `pdagent init` writes it, so reaching it means someone
set it empty deliberately -- and serving an unauthenticated enqueue
endpoint is not a reasonable reading of that. It now refuses and logs
why. The token comparison was also byte-by-byte, which leaks a shared
secret to anyone who can time the response, and the presented credential
was written to a world-readable log on every failure.

GenerateKey drew from math/rand seeded with the wall-clock nanosecond at
process start. The 36^32 keyspace is irrelevant when the real entropy is
one timestamp that can be narrowed from a file mtime.

send_handler indexed req.Header["Pd-Event-Version"][0], which panics on a
missing header, and logged the full request body at debug -- including
the routing key, into a log created world-readable.

Config permissions needed care rather than a chmod. viper writes 0644 and
the file holds the daemon's auth secret, but the CLI runs as the
monitoring user and reaches it only through that world-read bit. Removing
it naively would have left the CLI unable to enqueue, which stops paging
silently -- the exact failure mode being fixed elsewhere in this commit.
Group ownership now carries the access where a monitoring user exists,
with a fallback to owner-only where one does not. Verified on RHEL 8: the
file ends up 0640 pdagent:naemon, the CLI still reports healthy, and
world access is gone. Residual worth stating: apache is a member of the
naemon group on these hosts, so it can still read the file -- acceptable
given it already holds command-pipe write access, which is strictly more
power.

pkg/server had no tests. These are the first, covering the two behaviour
changes above.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens the PagerDuty agent’s “page delivery” path by addressing several inherited reliability and security defects across packaging, systemd, server middleware, and key generation.

Changes:

  • Improve service reliability via systemd start-limit placement and adding Restart=on-failure.
  • Harden the /send request path by preventing credential/body logging, avoiding header-index panics, refusing empty shared secrets, and using constant-time token comparison.
  • Secure secret generation and reduce config-file secret exposure via postinstall permission tightening and new server middleware tests.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
scripts/rpm/postinstall.sh Adjust config ownership/permissions to reduce secret exposure while keeping CLI access.
scripts/deb/postinstall.sh Same as RPM postinstall, for Debian packaging.
pkg/server/send_handler.go Stop logging request bodies; safer header access for event version.
pkg/server/middleware.go Refuse empty secrets; constant-time auth compare; avoid logging bearer credentials.
pkg/server/middleware_test.go Add tests for the new auth behavior and logging constraints.
pkg/common/helpers.go Switch key generation from math/rand to CSPRNG-backed randomness.
init/pdagent.service Add restart policy and correctly place StartLimit* in [Unit].
cmd/init.go Tighten config file permissions immediately after viper writes it.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread pkg/server/send_handler.go Outdated
Comment thread scripts/deb/postinstall.sh Outdated
Comment thread cmd/init.go Outdated
Comment thread scripts/rpm/postinstall.sh Outdated
Four findings, all real, and one of them a regression this branch
introduced.

Tightening the config to 0600 in `pdagent init` without reassigning
ownership would have left a root-created file the daemon could not read.
It would then have fallen back to a freshly generated secret, because the
config read error is discarded, and the CLI would have failed to
authenticate with nothing indicating why. Ownership now moves with the
mode, mirroring the postinstall so both paths converge.

Proving that also turned up a second problem in the same function. The
config directory was created 0744, which grants read without traverse --
no access at all for anyone but the owner. It never bit because the
package ships /etc/pdagent at 0755 and MkdirAll is a no-op when the
directory exists, so only a config created outside the package was
affected. Now 0755, matching what is shipped.

The postinstall comment claimed an owner-only fallback where no
monitoring group exists, but the code applied 0640 unconditionally,
leaving group read for the pdagent group. The fallback is now real.

An unrecognised Pd-Event-Version mapped to the zero value and failed
downstream as a 500, reporting a server fault for a malformed request and
telling the caller nothing. Rejected at the handler with a 400 naming the
accepted values.

Verified on RHEL 8: config lands 0640 pdagent:naemon in a 0755 directory,
the daemon and the monitoring CLI can both read it, and world access is
gone.
@eschoeller
eschoeller merged commit 478f303 into main Aug 4, 2026
4 checks passed
@eschoeller
eschoeller deleted the SEPE-1165_inherited_defects branch August 4, 2026 20:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants