Example

Isolated execution for the DataNivra agent

Run every row-level stage of your agent's jobs — profiling, classification, subsetting, masking, synthesis, validation and provisioning — in a fresh, hardened, short-lived container instead of the agent process. Each worker sees only its stage's input and output, this job's own connection details and the keys this job's masking policy names: never the agent's identity key, its control-plane token, your secret store, the container runtime or other jobs. Published data is byte-for-byte the same as without isolation.

Status
Preview
Where it runs
your agent host (Linux)
Needs
Docker ≥ 24 or Podman ≥ 4 (gVisor: runsc; Firecracker: KVM)

About this example

Run every row-level stage of your agent's jobs — profiling, classification, subsetting, masking, synthesis, validation and provisioning — in a fresh, hardened, short-lived container instead of the agent process. Each worker sees only its stage's input and output, this job's own connection details and the keys this job's masking policy names: never the agent's identity key, its control-plane token, your secret store, the container runtime or other jobs. Published data is byte-for-byte the same as without isolation.

Full guide: Isolated execution.

1. Configure the agent

Copy agent.isolation.env.example into your agent's environment and fill in the image digest of your agent release (see Packages and downloads). Run the agent as an unprivileged user that may use a rootless Docker or Podman.

2. Give network stages a narrow network (optional)

Stages that read a network source or write a network target join an operator network; every other stage runs with no network at all. create-worker-networks.sh creates the two networks and prints the firewall rules (for you to review and apply) that let them reach only the systems you name — nothing else on the network, nothing on the Docker host itself, and DNS only through the resolvers you list:

./create-worker-networks.sh --source 10.20.0.15:5432 --target 10.30.0.8:5432 --resolver 10.0.0.2

Prefer an internal resolver that answers only for your own zones, or use IP addresses in connection documents and omit --resolver.

3. Check the host

datanivra-agent isolation doctor        # every check must pass
./verify-worker.sh                      # while a job runs: shows each worker's hardening

4. Require it for your organization

An organization admin sets the minimum (console Organization → Execution isolation, or datanivra org isolation-update --min-isolation CONTAINER). Jobs then go only to agents that meet it, and requests can ask for more isolation, never less:

datanivra test-data request --profile-ref healthcare-integration@1 --min-isolation KERNEL_BOUNDARY --isolation-backend GVISOR

No data is included in this example: it configures and checks the agent host only.

Files

Exactly as they are in the examples repository. Every value is a synthetic placeholder; nothing here is a credential.

agent.isolation.env.example

# DataNivra agent — isolated execution (ADR-0022). Copy into the agent's environment.
# Values are configuration only; credentials stay in your secret store (secret references).

# OCI (Docker/Podman), GVISOR (runsc registered with Docker) or FIRECRACKER (KVM). Default: STANDARD.
DATANIVRA_AGENT_ISOLATION_BACKEND=OCI

# The worker image: your agent release, pinned by digest (never a tag).
DATANIVRA_AGENT_ISOLATION_IMAGE=ghcr.io/manuelbomi/datanivra-agent@sha256:<digest-from-downloads>

# Container CLI the agent uses (docker or podman; a path is allowed).
DATANIVRA_AGENT_ISOLATION_RUNTIME_CLI=docker

# Networks for stages that read network sources / write network targets (see create-worker-networks.sh).
# Leave unset when all sources are files: such stages are then refused instead of getting a network.
DATANIVRA_AGENT_ISOLATION_SOURCE_NETWORK=dn-sources
DATANIVRA_AGENT_ISOLATION_TARGET_NETWORK=dn-targets

# Per-worker limits (defaults shown).
DATANIVRA_AGENT_ISOLATION_MEMORY_BYTES=4294967296
DATANIVRA_AGENT_ISOLATION_CPUS=2
DATANIVRA_AGENT_ISOLATION_PIDS_LIMIT=512
DATANIVRA_AGENT_ISOLATION_STAGE_TIMEOUT_SECONDS=21600

# Optional: refuse images not signed by the DataNivra release workflow (needs cosign on the host).
# Use the signer identity and issuer published in the release's release-manifest.json (Downloads page).
# DATANIVRA_AGENT_ISOLATION_REQUIRE_IMAGE_SIGNATURE=true
# DATANIVRA_AGENT_ISOLATION_COSIGN_IDENTITY=<signing.certificate_identity from release-manifest.json>
# DATANIVRA_AGENT_ISOLATION_COSIGN_ISSUER=https://token.actions.githubusercontent.com

# Optional local floor: this agent refuses jobs below this level, whatever is requested.
# DATANIVRA_AGENT_ISOLATION_MINIMUM_LEVEL=CONTAINER

examples/agent-isolation/agent.isolation.env.example

create-worker-networks.sh

#!/usr/bin/env bash
# Create the two container networks isolated DataNivra workers use for network sources and targets, and print
# (never apply) firewall rules that limit each network to the systems you name: forwarded traffic (DOCKER-USER)
# to the listed destinations only, and nothing to services on the Docker host itself (INPUT).
#
#   ./create-worker-networks.sh --source HOST:PORT [--source ...] --target HOST:PORT [--target ...] \
#       [--resolver IP ...]
#
# DNS: workers may resolve names only through the resolvers you list with --resolver (UDP/TCP 53); without one,
# no DNS is allowed and connection documents must use IP addresses. Prefer an internal resolver that answers only
# for your own zones: an open recursive resolver can carry data out in DNS queries.
#
# Review the printed rules, then apply them with your host firewall tooling. Stages without a network source or
# target never join these networks (they run with --network none).
set -euo pipefail
cli="${DATANIVRA_AGENT_ISOLATION_RUNTIME_CLI:-docker}"
sources=(); targets=(); resolvers=()
while [ $# -gt 0 ]; do
  case "$1" in
    --source) sources+=("${2:-}"); shift 2 ;;
    --target) targets+=("${2:-}"); shift 2 ;;
    --resolver) resolvers+=("${2:-}"); shift 2 ;;
    *) echo "unknown argument: $1" >&2; exit 64 ;;
  esac
done
for ip in "${resolvers[@]}"; do
  if ! [[ "$ip" =~ ^[0-9]{1,3}(\.[0-9]{1,3}){3}$ ]]; then echo "expected an IPv4 resolver, got '$ip'" >&2; exit 64; fi
done
for dest in "${sources[@]}" "${targets[@]}"; do
  if ! [[ "$dest" =~ ^[0-9]{1,3}(\.[0-9]{1,3}){3}(/[0-9]{1,2})?:[0-9]{1,5}$ ]]; then
    echo "expected IPv4[/prefix]:PORT, got '$dest'" >&2; exit 64
  fi
done
create() {  # name subnet
  if "$cli" network inspect "$1" >/dev/null 2>&1; then
    echo "network $1 exists"
  else
    "$cli" network create --driver bridge --subnet "$2" --opt com.docker.network.bridge.enable_icc=false "$1" >/dev/null
    echo "created network $1 ($2)"
  fi
}
rules() {  # chain subnet destinations...
  # A dedicated chain (allow list, then DROP) entered by a jump inserted at the TOP of DOCKER-USER, so an existing
  # RETURN or ACCEPT later in DOCKER-USER cannot let unmatched worker traffic through.
  local chain="$1" subnet="$2"; shift 2
  echo "iptables -N $chain 2>/dev/null || iptables -F $chain"
  for dest in "$@"; do
    echo "iptables -A $chain -d ${dest%:*} -p tcp --dport ${dest##*:} -j ACCEPT"
  done
  for ip in "${resolvers[@]}"; do
    echo "iptables -A $chain -d $ip -p udp --dport 53 -j ACCEPT"
    echo "iptables -A $chain -d $ip -p tcp --dport 53 -j ACCEPT"
  done
  echo "iptables -A $chain -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT"
  echo "iptables -A $chain -j DROP"
  echo "iptables -C DOCKER-USER -s $subnet -j $chain 2>/dev/null || iptables -I DOCKER-USER 1 -s $subnet -j $chain"
  # DOCKER-USER sees forwarded traffic only; connections to the Docker host itself (e.g. via the bridge
  # gateway) traverse INPUT: deny them too, keeping only replies to connections the host opened.
  echo "iptables -N $chain-IN 2>/dev/null || iptables -F $chain-IN"
  echo "iptables -A $chain-IN -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT"
  echo "iptables -A $chain-IN -j DROP"
  echo "iptables -C INPUT -s $subnet -j $chain-IN 2>/dev/null || iptables -I INPUT 1 -s $subnet -j $chain-IN"
}
create dn-sources 172.31.250.0/24
create dn-targets 172.31.251.0/24
echo
[ ${#resolvers[@]} -eq 0 ] && echo "# No --resolver given: workers on these networks get no DNS (use IP addresses)."
echo "# Review, then apply (sources). Afterwards verify: a listed source connects, 1.1.1.1:443 and"
echo "# 169.254.169.254:80 do not (e.g. docker run --rm --network dn-sources <image> -c '...')."
rules DN-ISO-SOURCES 172.31.250.0/24 "${sources[@]}"
echo "# Review, then apply (targets):"
rules DN-ISO-TARGETS 172.31.251.0/24 "${targets[@]}"

examples/agent-isolation/create-worker-networks.sh

verify-worker.sh

#!/usr/bin/env bash
# Show the hardening of every running DataNivra worker container on this host (run while a job runs).
# Expected: ReadonlyRootfs=true, CapDrop=[ALL], no-new-privileges, Privileged=false, LogConfig=none,
# NetworkMode=none (or your source/target network) and no runtime socket among the mounts.
set -euo pipefail
cli="${DATANIVRA_AGENT_ISOLATION_RUNTIME_CLI:-docker}"
names=$("$cli" ps --filter label=io.datanivra.isolation=worker --format '{{.Names}}')
if [ -z "$names" ]; then
  echo "no worker is running right now (start a job, then run this again)"; exit 0
fi
for name in $names; do
  "$cli" inspect --format \
    '{{.Name}} stage={{index .Config.Labels "io.datanivra.stage"}} readonly={{.HostConfig.ReadonlyRootfs}} capdrop={{.HostConfig.CapDrop}} secopt={{.HostConfig.SecurityOpt}} privileged={{.HostConfig.Privileged}} network={{.HostConfig.NetworkMode}} logs={{.HostConfig.LogConfig.Type}} user={{.Config.User}}' \
    "$name"
  if "$cli" inspect --format '{{range .Mounts}}{{.Source}} {{end}}' "$name" | grep -q 'docker.sock\|podman.sock\|containerd.sock'; then
    echo "  WARNING: a runtime socket is mounted into $name"; exit 1
  fi
done

examples/agent-isolation/verify-worker.sh