Docs / Process firewall

Process firewall

On supported Linux hosts, Dynv can deny direct execve of non-allowlisted path-based files, TCP connect/bind to non-allowlisted ports, selected path-based writes, and selected IPC/syscalls. This reduces common direct post-exploit actions, including those taken by a malicious or compromised dependency with no known CVE.

What it does

dynv run already starts your service. With a firewall policy it also applies kernel controls before your command starts. These are operation-specific controls, not a complete sandbox:

  • Path-based exec allowlist. Direct path-based execution is limited to your runtime, app and declared helpers. A direct attempt to run /bin/sh, curl or a dropped binary outside those paths can be denied. Allowed ELF loaders and interpreters can still run other code; memfd, native libraries and JIT are outside this path rule.
  • TCP port allowlist. Direct TCP connect and bind are limited to declared ports for IPv4 and IPv6. This does not select hosts, and does not cover SCTP, ICMP, UDP, DNS, inherited sockets or sockets received over IPC.
  • Optional path-based write allowlist. Selected filesystem writes can be limited to declared directories (uploads, tmp, cache). Already-open files and other IPC are separate channels.

An unpatched app with an RCE. Common next steps such as directly running a shell or downloader, making a direct TCP connection on an unexpected port, or writing outside declared paths can be denied. This reduces direct post-exploit activity; it does not remove the attacker's ability to run code inside your allowed runtime.

A malicious or compromised dependency. The same operation-specific controls apply whether the code arrived through an exploit or npm install, with no CVE required. They can reduce a package's direct path-exec, TCP-port and path-write actions; they cannot make every exfiltration or execution route impossible.

It pairs with vulnerability monitoring: monitoring tells you when a package is known bad; the firewall reduces selected actions a package can take, known-bad or not. Observation and violation reports are best effort.

Modes

ModeWhat happens
offNo policy, no protection. The service runs as before.
learnObserves and records what the service executes, connects to and binds. It doesn't apply the policy's exec/port/write denials; the seccomp hardening deny-list (session escape, socket families/protocols, memfd/execveat when exec is restricted) still applies. After a while, dynv firewall propose turns what it saw into a policy you review.
alertObserves and reports what enforce would have blocked. It doesn't apply the policy's exec/port/write denials; the seccomp hardening deny-list (session escape, socket families/protocols, memfd/execveat when exec is restricted) still applies.
enforceBlocks; reports when the observer is enabled.

Start in learn: leave the service running through a normal working period, propose, then review. The proposal merges what every reporting replica saw and shows what backs each rule ("seen 1,204 times on 3 instances"). Anything that looks like post-exploitation is never ticked for you: shells (sh, bash, zsh…), network tools (curl, wget, nc, socat, ssh…) and interpreters other than the service's own are proposed unchecked with a warning, so you have to allow them deliberately.

Templates give you a hand-written starting point instead: node-web, php-fpm, python-wsgi and go-binary, each with its interpreter paths and typical ports. A template is copied into the policy, not referenced, so the signed policy is always complete.

The policy

The policy is a firewall field in the service's document. Once it is saved, dynv run applies it; no extra flag turns protection on.

firewall
{
  "v": 1,
  "mode": "enforce",
  "learn_until": 1731000000,
  "exec": {
    "allow": ["/usr/local/bin/node", "/app", "/usr/bin/convert"],
    "allow_system": "interpreter"
  },
  "net": {
    "connect_tcp": [5432, 443, 587],
    "bind_tcp": [3000],
    "deny_udp": false
  },
  "fs": { "write": ["/app/uploads", "/tmp"] },
  "on_violation": "report"
}
FieldMeaning
modeoff, learn, alert or enforce.
learn_untilIn learn mode, when to stop proposing (informational).
exec.allowAbsolute paths that may be executed. A directory allows everything beneath it. allow_system: "interpreter" adds the started binary and its ELF interpreter; shared libraries need no rule.
net.connect_tcpPorts the service may connect to, for IPv4 and IPv6.
net.bind_tcpPorts the service may listen on.
net.deny_udpRefuse UDP sockets. Off by default, because DNS then has to go over TCP or through the local resolver socket. See what it can't do.
fs.writeDirectories the service may write to. Absent means writes are not restricted.
on_violationreport, or report_and_kill to stop the process that attempted an action the kernel actually denied. Actions that were only observed or would-block never kill.
socket_activationOff by default: inherited descriptors are close-on-exec, so a connected socket passed by accident cannot bypass the connect rules. Set true to keep fds handed in deliberately (systemd socket activation).

Entries are resolved when the rules are applied, and the status report lists each resolved path, so a surprising symlink in your image is visible. Any policy change restarts the service, exactly like a restart variable: a tightening (removing a path or port, alert → enforce) is in force about a second after it is saved, and so is a change to what is observed, because the observer's classifier is built for one policy. A replacement is armed before the running service is stopped: a policy this host cannot apply leaves the healthy service running.

CLI

sh
dynv firewall status|learn|alert|off|propose|apply|violations|feed [--process P] [--json]

A typical first policy, on a service you have just wrapped with dynv run:

sh
# observe for a working period; the policy's denials are not applied
dynv firewall learn --process web

# see what normal behaviour suggests, then review and apply it
dynv firewall propose --process web
dynv firewall apply --process web

# what this instance actually enforces, and what it stopped
dynv firewall status --process web
dynv firewall violations --process web --json

Applying a proposal is a normal signed write of the service document, so it needs a writer token. The dashboard offers the same flow in a Firewall tab, with the allowlist editor, the proposal review and the violations feed; MCP exposes get_firewall_policy, list_firewall_violations and propose_firewall_policy read-only, and apply_firewall_policy only with --allow-firewall-write.

What is actually enforced

Not every kernel can enforce every layer, so Dynv measures what this instance actually applied, at start, after the restrictions were in place. The mode the policy asks for and the level the instance achieved are always shown side by side. Dynv never claims protection the kernel is not enforcing: a service that asks for enforce and only reaches monitor gets a warning badge, never a green one.

LevelShown asMeaning
fullEnforced: exec, ports, writesEvery layer in the policy is armed — each for the operations it covers. Not a claim of full containment.
exec_onlyEnforced: exec (writes only if listed; ports not enforced: kernel < 6.7)Execution is enforced; writes only when the policy has an fs write layer; TCP ports are observed only.
monitorMonitoring only: not applying the policy denialsThe service's activity is logged and reported. It doesn't apply the policy's exec/port/write denials; the seccomp hardening deny-list (session escape, socket families/protocols, memfd/execveat when exec is restricted) still applies.
noneNot protectedNo kernel support for either layer. Without fail_open an enforce policy is refused; with it, the service starts unprotected. On macOS there is no firewall reporter.
off—No policy, or mode: off.
EnvironmentLevel
Linux ≥ 6.12 with Landlock enabled (Fedora, Ubuntu 24.10+, Debian 13)full, including IPC scoping
Linux 6.7–6.11full (no IPC scoping)
Linux 5.13–6.6 (Ubuntu 22.04 HWE, Debian 12, RHEL 9)exec_only: an enforce policy that lists ports refuses to start unless fail_open
Landlock compiled but not enabled in lsm= (some RHEL and Amazon Linux defaults)monitor
Docker ≥ 23 default profile, Kubernetes RuntimeDefaultAs the host
gVisor, macOSnone

full means every layer the policy lists is armed on this kernel; it is not a claim that the service is contained. Each layer covers only its own operations: direct path-based execution of the paths you allowed (not an allowed loader's target, memfd, native libraries or JIT), direct TCP connect/bind on IPv4 and IPv6 (not SCTP, ICMP, UDP, DNS, or inherited and received sockets), the path writes Landlock checks, and the selected IPC/syscall controls. See what it can't do.

In enforce mode, Dynv refuses to run a service without the protection it asked for. On Linux, a child pre-exec or spawn failure reports enforce_failed. An arm-time refusal — including an unsupported policy or a non-Linux host — is a supervisor startup error, not a child report; non-Linux has no firewall reporter. fail_open: true explicitly runs anyway at the achieved level, reported as degraded where firewall reporting exists; non-Linux runs unprotected with no firewall reporter (there is no CLI override).

Where the policy lives

The policy is a field in your signed, encrypted service document, not a server-side setting. It is encrypted under your service's data key, signed by your writer, and verified by dynv run before it is applied. That means:

  • The server cannot weaken it. It can withhold an update, which at worst keeps an older, still-signed policy.
  • It is delivered live through the same watch that delivers variables.
  • Only the service can read it, because the service must enforce it.

An older CLI that does not know the field can strip it when it re-signs the document. After authenticating and decrypting an update, dynv run refuses it if it removes a feature present in the last accepted document, or drops the policy field while retaining its marker. The only permitted security-policy removal is an explicit firewall policy with mode: off (written by dynv firewall off); the separate fallbacks lifecycle may still remove its marker when the effective process-plus-group document has no fallback behaviour left. The accepted features are restored from the cached signed document when that local cache and floor are trustworthy, so this protects live updates and restarts while they are intact; the sealed local sidecar is only best-effort recovery data. Current admin clients also refuse to save a document whose last version had the firewall feature unless they understand it. An older dynv run simply ignores the policy and runs unprotected; because it sends no firewall status, the dashboard shows no report, never "protected".

Privacy

Violations are as sensitive as a package inventory: the destinations and paths a service talks to describe your infrastructure and your attackers. So the firewall uses the same sealed pattern as vulnerability monitoring. The service writes a padded status report under a key derived from its data key; the dashboard and CLI decrypt it with your account key.

Alerts work like vulnerability alerts, with the same channels, severity thresholds, mute and rate limits. The text a server can build is counts, the mode and the level, plus a link — for example "1 blocked program execution, 2 outbound connections in service pKx… (mode enforce, level full)". When nothing armed denied the action it says "would have blocked" instead, and it never says either for an allowed action. Starting the service does not alert.

Residual risks, stated plainly:

  • A compromised service can lie about its own reports and send zero violations. It cannot loosen the kernel rules, which were applied before the attacker's code ran, and it cannot touch another service.
  • Alerts are opaque. Counts and a link are less convenient than a destination; the dashboard and CLI have the details.

What it can't do

  • Allowed loaders and interpreters remain execution routes. An allowed ELF loader can run another program, and an allowed interpreter (node -e, php -r, python -c) runs attacker code. The path allowlist is not a complete execution boundary.
  • memfd, native libraries and JIT bypass path rules. Execution via memfd_create/execveat or /proc/self/fd, loading a dropped native library, and anonymous executable memory are not comprehensively covered by path-based Landlock rules. Keep write directories few and mount them noexec where possible.
  • TCP Fast Open is controlled; other protocol tricks are not. sendto, sendmsg and sendmmsg with MSG_FASTOPEN connect without connect(2); when a net block exists they are refused in enforce; reported when the observer runs. Anything else that opens a conversation outside connect(2) is covered only by the same defence-in-depth approach.
  • A registered binfmt_misc handler can run an interpreter on a file you allowed. For example an aarch64 ELF in an allowed directory runs qemu-aarch64-static. The kernel starts the handler, and a path allowlist cannot judge it. Proposals warn about writable allowed directories, and the status lists the handlers it finds (sorted; it says when there are more than it lists).
  • TCP port mode cannot tell one host from another. Allowing outbound 443 allows any host on 443, including an attacker's. Hostname mode is future work.
  • SCTP, ICMP, UDP and DNS are outside TCP port control. Seccomp filters selected socket types as defence in depth; deny_udp: true optionally refuses inet UDP sockets. DNS tunnelling through an allowed resolver remains possible.
  • Inherited and received sockets bypass connect-time rules. Deliberately passed descriptors (including socket activation), a connected descriptor received with SCM_RIGHTS, and path-based Unix sockets or other host IPC can carry traffic outside the TCP connect allowlist.
  • Detached and pre-existing descendants can outlive the service. Process-group signalling is not complete containment. A descendant that changes session/process group, or one that predates supervision, can survive restart or shutdown; cgroup/PID-namespace containment is future work where privileges permit.
  • Same-UID local state can reset the strip history. A process with the service's user can delete or roll back the cache and floor, or restore both the cache and sidecar consistently to an older accepted state. That can reset the accepted-feature history or re-allow a path that was previously denied, and Dynv cannot prevent it. With no cache there is no persisted history to rely on in the first place.
  • Local recovery and tamper detection are best effort. The signed-document feature floor is the strip defence. A sealed sidecar is loaded only when a policy floor was recorded; unrelated or cross-process files do not block boot. If a recorded copy is missing, edited or rolled back, it is reported as policy_tampered when loaded; with no recorded floor there is nothing to load or report. An fs write boundary protects the actual sidecar only when enforce-mode Landlock is applied.
  • Learning and reports are not proof of protection. Observation is best effort and can be incomplete or inaccurate. A dependency already malicious during learning has its actions learned as normal; review proposals rather than treating learning as a trust boundary.

Last updated