mirror of
https://github.com/hanzoai/egress.git
synced 2026-10-30 15:08:38 +00:00
Egress writes no file, refuses core dumps, mounts no writable path and hides /proc — and none of that governs the kernel, which pages anonymous memory to swap whenever it likes. A key held only in RAM is still a key on a disk once it has been swapped, and it stays there after the process is gone. That was the last way a credential reached one. It now calls mlockall(MCL_CURRENT|MCL_FUTURE) BEFORE opening the store, because the credential does not exist yet at that point and locking after a secret is in memory locks it too late. MCL_FUTURE is the half that matters for the same reason. The unit grants LimitMEMLOCK=infinity; without it the process refuses to start rather than serving without the guarantee, since "best effort" means it holds on some hosts and not others with nothing to say which. Memory encryption is reported rather than assumed: pinning keeps a key off the disk and does nothing about the DRAM, so the boot line names sev-snp, tdx or none and a deployment that needs one can be held to it. Off Linux these pages cannot be pinned, so egress refuses there rather than serving quietly without the property it is built on. The deploy doc states the other two guarantees that were true and unwritten: nothing is kept, so replicas are interchangeable and a suspect one is deleted rather than investigated; and --with-key is an operator's choice between host and tpm2 that egress never learns, with a cloud KMS or PKCS#11 HSM named as the integration it would take rather than a flag that exists.
88 lines
3.3 KiB
Desktop File
88 lines
3.3 KiB
Desktop File
[Unit]
|
|
Description=Hanzo Egress — the outbound trust boundary
|
|
Documentation=https://git.hanzo.ai/hanzoai/egress
|
|
After=network-online.target
|
|
Wants=network-online.target
|
|
|
|
[Service]
|
|
Type=exec
|
|
ExecStart=/usr/local/bin/egress
|
|
Restart=always
|
|
RestartSec=2s
|
|
|
|
# Configuration arrives from this host and nowhere else, and it is only
|
|
# configuration: the identity moved to the sealed credential below, so this file
|
|
# now holds no secret at all. Root-owned 0400 regardless — an endpoint and an
|
|
# audience still describe where this process spends.
|
|
EnvironmentFile=/etc/egress/env
|
|
|
|
# THE IDENTITY, DECRYPTED INTO MEMORY AND NOWHERE ELSE.
|
|
#
|
|
# systemd decrypts this and places it in %d, a per-service directory that is
|
|
# memory-backed and readable only by this unit. So it is ciphertext on disk,
|
|
# plaintext only inside this process, and absent from the environment — where
|
|
# /proc/<pid>/environ would otherwise hand it to anything running as root.
|
|
#
|
|
# --with-key=host binds it to /var/lib/systemd/credential.secret, which lives on
|
|
# the LUKS root: ciphertext at rest twice over, and a disk read without the
|
|
# passphrase yields neither. Encrypt it with:
|
|
# systemd-creds encrypt --with-key=host --name=mnemonic - /etc/egress/mnemonic.cred
|
|
LoadCredentialEncrypted=mnemonic:/etc/egress/mnemonic.cred
|
|
# The http transport's secret, encrypted the same way. Only the one the endpoint
|
|
# selects is read, and a missing one is a refusal rather than a fallback.
|
|
LoadCredentialEncrypted=client-secret:/etc/egress/client-secret.cred
|
|
# THE KEY THAT OPENS EVERY PROVIDER CREDENTIAL. Its public half, EGRESS_RECIPIENT
|
|
# in the environment file, seals them, and lives openly wherever it likes —
|
|
# nobody who holds it can read anything. This half is why: it exists on this
|
|
# machine, encrypted to this machine, and every credential in KMS is ciphertext
|
|
# to everyone who does not have it. Mint the pair once, keep this half here and
|
|
# nowhere else:
|
|
# systemd-creds encrypt --with-key=host --name=identity - /etc/egress/identity.cred
|
|
LoadCredentialEncrypted=identity:/etc/egress/identity.cred
|
|
|
|
User=egress
|
|
Group=egress
|
|
DynamicUser=no
|
|
|
|
# A provider key exists only in this process's memory. These stop it reaching
|
|
# anywhere it could be read back.
|
|
#
|
|
# LimitMEMLOCK is what lets the process pin every page it holds: without it
|
|
# mlockall fails, egress refuses to start, and it refuses on purpose — the
|
|
# kernel may page anonymous memory to swap whenever it likes, and a key that has
|
|
# been swapped is a key on a disk that outlives the process.
|
|
LimitMEMLOCK=infinity
|
|
LimitCORE=0
|
|
PrivateTmp=yes
|
|
PrivateDevices=yes
|
|
ProtectSystem=strict
|
|
ProtectHome=yes
|
|
ProtectProc=invisible
|
|
ProcSubset=pid
|
|
ProtectKernelTunables=yes
|
|
ProtectKernelModules=yes
|
|
ProtectKernelLogs=yes
|
|
ProtectControlGroups=yes
|
|
ProtectClock=yes
|
|
ProtectHostname=yes
|
|
RestrictNamespaces=yes
|
|
RestrictRealtime=yes
|
|
RestrictSUIDSGID=yes
|
|
RestrictAddressFamilies=AF_INET AF_INET6
|
|
LockPersonality=yes
|
|
MemoryDenyWriteExecute=yes
|
|
NoNewPrivileges=yes
|
|
CapabilityBoundingSet=
|
|
AmbientCapabilities=
|
|
SystemCallArchitectures=native
|
|
SystemCallFilter=@system-service
|
|
SystemCallFilter=~@debug @mount @obsolete @privileged @resources
|
|
UMask=0077
|
|
|
|
# No writable path at all: egress writes no files by design, and a process that
|
|
# cannot write cannot spill a credential to disk.
|
|
ReadWritePaths=
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|