Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changes/unreleased/+coarse-application-network-policy.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
kind: Security
body: 'Deny public and local application networking by default, conservatively require both grants for ambiguous translation and tunneling ranges, select host-derived or public DNS from those grants, and enforce the policy for workloads, commands, shells, and lifecycle actions while preserving declared inbound endpoints.'
8 changes: 8 additions & 0 deletions .github/workflows/integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,14 @@ jobs:
go test -timeout 10m ./internal/dockerdeploy
-run '^(TestPrivateWorkloadEnvironmentDockerIntegrationMasksFilesAndInjectsValues|TestPrivateRuntimeMasksDockerIntegrationProtectTransientContainer|TestPrivateWorkloadEnvironmentRealDockerIsolation)$'

- name: Run application network-policy integration tests
if: matrix.name == 'Linux amd64'
env:
REPLOY_DOCKER_INTEGRATION: "1"
run: >-
go test -timeout 10m ./internal/dockerdeploy
-run '^TestApplicationNetworkPolicyDockerIntegration$'

- name: Run CLI runtime integration
if: ${{ matrix.persistent_install == false }}
env:
Expand Down
29 changes: 22 additions & 7 deletions docs/APT_PROVIDER_DETAIL_DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -2127,6 +2127,8 @@ intersections with reserved system, Reploy, or provider paths are invalid.
```go
type RuntimePolicyV1 struct {
Schema string
StartupVerifier ApplicationStartupVerifierV1
Network RuntimeNetwork
ProtectedPaths []ProtectedPathV1
Plans []RuntimePlanV1
}
Expand All @@ -2139,6 +2141,7 @@ type ProtectedPathV1 struct {

type RuntimePlanV1 struct {
ID string
InboundTCP []string
Mounts []RuntimeMountV1
Executables []QualifiedOutput
}
Expand All @@ -2150,11 +2153,20 @@ type RuntimeMountV1 struct {
}
```

`Schema` is `runtime-policy-v1`. Protected paths are unique normalized absolute
paths sorted by path. Protected kind is `reploy-root`,
`Schema` is `runtime-policy-v1`. `StartupVerifier` identifies the exact trusted
application setup recipe embedded in the runtime image. `Network` records the
effective independent `public` and `local` access values, each `allow` or
`deny`, plus the `ambiguous` translation/tunneling policy, either `require-both`
or `allow`. IPv4-mapped IPv6 sockets follow their embedded IPv4 class because
Linux emits them as IPv4 packets. Protected paths are unique normalized
absolute paths sorted by path.
Protected kind is `reploy-root`,
`provider-root`, `provider-leaf`, or `executable-path`; owner is the stable node
or qualified-output identity. Plans use the stable command/workload/probe ID and
are sorted by ID. Mounts are sorted by destination; `SourceKind` records only
are sorted by ID. `InboundTCP` records sorted unique canonical decimal port
strings for that exact container shape: declared ports belong only to the
workload plan, while shell and transient command plans use an empty array. Mounts are sorted by
destination; `SourceKind` records only
the resolved kind (`file`, `directory`, or `generated`) and never a host source
path. Executables are unique and sorted by qualified identity.

Expand All @@ -2169,10 +2181,13 @@ build identity input.
Each one-shot command and `reploy shell` mounts a fresh 64 MiB tmpfs at
`/mnt/reploy-home` for `HOME` and `TMPDIR`. The mount is mode `0700`, owned by
the selected runtime UID/GID, and removed with the transient container. Docker
starts the resolved executable directly under that final numeric identity; no
root bootstrap helper is involved. Explicit interruption cleanup force-removes
the transient container. Workload containers use the same bounded tmpfs-home
policy at `/mnt/reploy-home`.
starts only the trusted Reploy helper as container root with the minimal setup
capabilities. The helper installs any required application-network rules,
assumes the selected numeric identity, irreversibly drops its setup authority,
verifies the final kernel state, and executes the resolved application argv.
Explicit interruption cleanup force-removes the transient container. Workload
containers use the same setup contract and bounded tmpfs-home policy at
`/mnt/reploy-home`.

The policy digest is
`canonical.Sum("runtime-policy", "runtime-policy-v1", policy)`. It is recorded
Expand Down
23 changes: 10 additions & 13 deletions docs/BACKLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,6 @@ This file is the day-to-day queue for design and implementation gaps.
stopping, and whether the next step needs user review, approval, input, or no
user action.

## Now

- [ ] `P1` Implement the initial coarse application-network policy. Preserve
independent public and local policy intent with both denied by default,
apply it consistently to workloads, commands, shells, and lifecycle
commands, and use only proven isolation and endpoint primitives from the
active runtime backend. Permit exact declared inbound endpoints without
granting general local access. Fail closed when a backend cannot realize
a requested combination, and do not represent this slice as destination-,
domain-, or packet-level filtering.

## Pre-release

- [ ] `P1` Accept APT install transaction records with the optional trailing
Expand Down Expand Up @@ -203,7 +192,13 @@ This file is the day-to-day queue for design and implementation gaps.
cases justify its scope, precedence, user/system ownership, validation,
and portability. Initial potential use case: overriding the otherwise
fixed host-owned limits for controlled-session endpoint streams and
connection-open rates.
connection-open rates. Also record host-owned DNS resolver configuration
used to provide DNS under the coarse application network grants. The
default local-capable path should use the host's configured resolver so
VPN and split-DNS behavior remains available; the public-only path should
use the built-in Google Public DNS profile (`8.8.8.8`, `8.8.4.4`). Allow
host configuration to override either choice. Resolver selection is
machine policy, not blueprint policy.

- [ ] `P2` Design and implement a Reploy userland L3 policy gateway. Keep this
separate from the initial public/local kill switches and controlled
Expand All @@ -217,7 +212,9 @@ This file is the day-to-day queue for design and implementation gaps.
initial controlled-session host-loopback endpoint publication so only the
lease-owned Host Reploy operation can reach the recorded application;
include multi-user-host tests proving unrelated local processes cannot
bypass the session endpoint grant.
bypass the session endpoint grant. Replace the temporary, discouraged
`environment.runtime.network.ambiguous: allow` escape hatch with precise
translated-destination policy and deprecate that coarse override.

- [ ] `P2` Evaluate and prioritize the Dingo development-environment gaps.
Use `docs/DINGO_GAPS.md` as the needs and evidence record for portable
Expand Down
71 changes: 70 additions & 1 deletion docs/BLUEPRINT_ENVIRONMENT_MODEL.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ environment:
allow_concurrent: auto # App-command and shell overlap policy.
runtime:
user: reploy # Container-local account name; defaults to reploy.
network:
public: deny # Public Internet access; defaults to deny.
local: deny # Local/private network access; defaults to deny.
ambiguous: require-both # Translation/tunnel ranges require both grants.
terminal: {} # Terminal/color integration.
install: {} # Installation target, identity, and success output.
mounts: {} # Runtime filesystem contracts.
Expand All @@ -106,7 +110,11 @@ one to 32 bytes, beginning with a lowercase ASCII letter or underscore, followed
only by lowercase ASCII letters, digits, underscores, or hyphens. `root` is
reserved and cannot be selected through this field. If the base image already
defines the same account name with a different numeric ID, runtime-layer
construction fails rather than rewriting that unrelated account. Backend-specific
construction fails rather than rewriting that unrelated account.
`runtime.network.public` and
`runtime.network.local` independently accept `allow` or `deny`; both default to
`deny`. `runtime.network.ambiguous` accepts `require-both` or the temporary,
discouraged `allow` escape hatch; it defaults to `require-both`. Backend-specific
runtime choices remain under the top-level `docker` node.

## Internal Execution Phases
Expand Down Expand Up @@ -1382,6 +1390,67 @@ contract is implemented. Reploy rejects these combinations before container
creation or output-path preparation. Docker-managed volumes and tmpfs remain
available because they do not expose a host filesystem path directly.

Application networking is also a portable environment policy rather than a
Docker mode. `public` controls globally routable IP destinations. `local`
controls private, link-local, multicast, reserved, and infrastructure metadata
destinations. Translation and tunneling ranges that can represent either class
are `ambiguous`: by default, `ambiguous: require-both` permits them only when
both `public` and `local` are allowed. IPv4-mapped IPv6 socket addresses use
the class of their embedded IPv4 destination because Linux emits them as IPv4
packets. Container-local loopback remains available and cannot address host
loopback through the container network namespace. The backend configures the
container's DNS path from the same grants. With neither network class granted,
DNS is unavailable. With only `local`, it uses the host's configured resolver
so local, VPN, and split-DNS behavior remains available. With only `public`, it
uses the built-in Google Public DNS profile (`8.8.8.8` and `8.8.4.4`). With
both, it uses the host resolver, which normally provides both local and public
resolution.

For Docker, the local-capable path leaves DNS selection to Docker so it derives
the container's resolver path from the host; the public-only path passes the
selected Google Public DNS profile through Docker's per-container DNS
configuration. Docker writes the resulting container resolver configuration.
The default bridge normally exposes host-derived resolver addresses, while a
custom network exposes Docker's embedded resolver at `127.0.0.11` and forwards
to the selected upstreams. Before installing the packet filter, Reploy's
trusted startup helper reads those engine-authored resolver addresses and
admits TCP and UDP port 53 only to them whenever either network class is
granted. This engine-owned exception does not grant general access to the
resolver's address class. Resolver selection is host policy rather than
blueprint policy; future Reploy host configuration may override the default
local and public resolver choices. The backend does not filter DNS answers.
Connections to every resolved address still pass the ordinary destination
policy, so an answer outside the granted address class remains unreachable.

`ambiguous: allow` is a temporary, discouraged escape hatch for environments
that intentionally need those translation or tunneling ranges. It grants every
range in that coarse class even when either ordinary network class is denied,
so it weakens the isolation expressed by `public` and `local`. Reploy expects to
deprecate this option after the planned L3 policy gateway can classify the real
destination instead of its translated address.

Declared workload endpoints remain reachable from their explicit host
publication, which uses loopback by default: the application firewall permits
new inbound TCP connections only to declared endpoint ports and permits the
corresponding established response traffic. When DNS is enabled, resolved
connections remain subject to the destination policy.

The Linux-container backend realizes this policy with a trusted Reploy startup
helper and IPv4/IPv6 nftables rules inside the container network namespace,
including when both egress classes are allowed so undeclared inbound ports
remain closed. Docker starts only that helper as container root with the minimal
setup capabilities. After installing the rules, the helper changes to the
planned application UID/GID, empties every capability set and the capability
bounding set, locks securebits and `no-new-privileges`, verifies seccomp and
the final kernel state, and executes the exact application argv. Reploy-issued
execs into an application container use the same authority-dropping helper;
they never invoke an application command through raw `docker exec`.

This is coarse IP-class enforcement. It is not domain, URL, DNS-content,
general outbound port policy, or packet auditing, and it does not defend a
container from an operator who already controls the Docker daemon. A backend
that cannot install and verify the requested policy fails closed.

This is a portable blueprint contract with target-specific realization. The
current backend writes Linux account databases. A future native-Windows or
other target backend may realize the same local identity through different OS
Expand Down
Loading
Loading