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,curlor 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
| Mode | What happens |
|---|---|
off | No policy, no protection. The service runs as before. |
learn | Observes 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. |
alert | Observes 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. |
enforce | Blocks; 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.
{
"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"
}| Field | Meaning |
|---|---|
mode | off, learn, alert or enforce. |
learn_until | In learn mode, when to stop proposing (informational). |
exec.allow | Absolute 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_tcp | Ports the service may connect to, for IPv4 and IPv6. |
net.bind_tcp | Ports the service may listen on. |
net.deny_udp | Refuse 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.write | Directories the service may write to. Absent means writes are not restricted. |
on_violation | report, 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_activation | Off 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
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:
# 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 --jsonApplying 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.
| Level | Shown as | Meaning |
|---|---|---|
full | Enforced: exec, ports, writes | Every layer in the policy is armed — each for the operations it covers. Not a claim of full containment. |
exec_only | Enforced: 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. |
monitor | Monitoring only: not applying the policy denials | The 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. |
none | Not protected | No 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. |
| Environment | Level |
|---|---|
| Linux ≥ 6.12 with Landlock enabled (Fedora, Ubuntu 24.10+, Debian 13) | full, including IPC scoping |
| Linux 6.7–6.11 | full (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 RuntimeDefault | As the host |
| gVisor, macOS | none |
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 viamemfd_create/execveator/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 themnoexecwhere possible.- TCP Fast Open is controlled; other protocol tricks are not.
sendto,sendmsgandsendmmsgwithMSG_FASTOPENconnect withoutconnect(2); when anetblock exists they are refused in enforce; reported when the observer runs. Anything else that opens a conversation outsideconnect(2)is covered only by the same defence-in-depth approach. - A registered
binfmt_mischandler can run an interpreter on a file you allowed. For example an aarch64 ELF in an allowed directory runsqemu-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
443allows 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: trueoptionally 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_tamperedwhen loaded; with no recorded floor there is nothing to load or report. Anfswrite 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