Skip to content

Host Settings: /proc and Core Dumps

Two host-wide settings complete the service hardening in the previous chapter: hiding the process table from other users, and making sure the Processor's memory is never written to disk.

Hide /proc from other users: hidepid=2

By default, Linux lets every user list every process and read a good deal about each one — including, for processes running as the same user, the full environment in /proc/<pid>/environ and the command line in /proc/<pid>/cmdline. ProtectProc=invisible in the unit hides other processes from the Processor; the host-wide hidepid option does the reverse and hides the Processor (and everything else) from every user that is not root or the process's owner.

Mount /proc with hidepid=2 in /etc/fstab:

proc  /proc  proc  defaults,hidepid=2,gid=procview  0  0

and apply it without a reboot:

sudo groupadd --system procview
sudo mount -o remount,hidepid=2,gid=procview /proc

hidepid=2 makes /proc/<pid> directories of other users' processes completely invisible — not just unreadable, but absent from a listing. A local user cannot even learn that pg_relay is running, let alone inspect it. The gid= option names one group that is exempt: add your monitoring agent's user to procview if it must see the process table, and nothing else. Without a gid= exemption, some distributions' systemd-logind and polkit need care under hidepid=2. Test the remount on a non-production host of the same build first, and check journalctl -b for permission errors after it.

The two directives are complementary, not redundant

ProtectProc=invisible is per-unit and protects the Processor's view; hidepid=2 is host-wide and protects the Processor from others' view. Use both. On a host that runs only the Processor, hidepid=2 also happens to hide it from any future service you add there before you remember to think about it.

Core dumps: disable them

A core dump is a copy of a process's memory written to disk at the moment it crashes. It captures everything the process had in memory, regardless of where it came from — the resolved _env: secrets pg_relay had just used for a send, the bearer token it fetched, the database credential libpq read from .pgpass, the content of the notification in flight. It does so with whatever file permissions the dump handler uses, in a location the process's sandbox does not govern, and it outlives the process. Nothing in the unit's sandbox protects against it, and the Processor cannot scrub its own memory before the kernel copies it. The only sound policy is that no dump is ever written.

Three layers, all of which should be set:

1. In the unit — already present above — LimitCORE=0 sets the core-file size limit to zero for the service, so the kernel produces no dump for a crash in the Processor at all.

2. In systemd-coredump, which on most distributions is the configured core_pattern handler and which ignores the process's RLIMIT_CORE when deciding whether to capture (it honours it only when deciding whether to store a file). Disable storage host-wide in /etc/systemd/coredump.conf:

[Coredump]
Storage=none
ProcessSizeMax=0

Storage=none means no dump is written to disk; ProcessSizeMax=0 means none is even collected into memory for the journal. Both are needed — with Storage=none alone, coredumpctl can still hold a dump in the journal.

3. In the kernel, prevent dumps of processes that changed credentials (which every User= service has):

echo 'fs.suid_dumpable = 0' | sudo tee /etc/sysctl.d/90-pg_relay.conf
sudo sysctl --system

Confirm the result: coredumpctl list should show nothing for pg_relay after a deliberate kill -SEGV of a test instance, and journalctl -u pg_relay should show the restart with no Core dumped line.

If your organisation requires dumps for post-mortem

The Processor has nothing in a dump that a support engineer would need — every failure it can hit is already written, with the reason, to pgrelay.log or the journal, and its own logs never contain secrets. Disable dumps for this service without exception, and treat "we keep dumps for everything" as a policy that needs an explicit carve-out here, not the other way round.


Continue to Database-Side Controls.