Skip to main content

Security & Hardening

CoderFlow runs AI agents inside Docker containers that hold credentials for your source code, your IBM i systems, and your AI providers. This page covers how to deploy it safely, how to verify your setup from the outside, and how to limit the damage if a container is ever compromised.

If you read only one section, read the first one. It describes a Docker behavior that has caused real-world incidents for administrators who reasonably believed their firewall protected them.

Docker Publishes Ports Around Your Firewall​

Host firewalls do not cover Docker published ports

When Docker publishes a container port, it inserts its own rules directly into the system's packet-forwarding chain. Those rules are evaluated before host firewalls such as ufw or firewalld ever see the traffic. A port published on all interfaces is reachable from the network even when your firewall says otherwise, and ufw status will not list it.

Never rely on a host firewall alone to protect a Docker host. Verify exposure from the outside, as shown below.

CoderFlow task containers publish ports for the in-browser editor and for application-server previews. These ports have no authentication of their own. All authentication happens in the CoderFlow server, which proxies browser traffic to them. CoderFlow binds all published container ports to 127.0.0.1, so they are reachable only by the CoderFlow server on the same host and never from the network.

Verify From the Outside​

Configuration review is not verification. Test what the network can actually reach:

  1. Port scan the host from outside its network (for example from a machine on a different network, or a cloud shell):

    nmap -p- your-server.example.com

    Only the ports you intentionally expose should appear, typically SSH and the CoderFlow server or reverse-proxy port. If unexpected high-numbered ports (32768 and up) appear, container ports are exposed.

  2. Confirm authentication is enforced on everything that is reachable:

    curl -i https://your-server.example.com/tasks

    The response must be a 401 or a redirect to the login page, never task data.

Repeat this check after Docker or CoderFlow upgrades and after firewall changes.

Run the CoderFlow server on a private network, not on the public internet. Remote users should reach it through your VPN. This removes the entire class of internet-borne attacks regardless of any single component's configuration.

Some integrations need inbound traffic from the internet (Git webhooks, Slack, Microsoft Teams, inbound automation webhooks). Do not expose the whole server for them:

  • Use the dedicated webhook-only ingress listener for inbound automation webhooks, or the messaging-integrations listener for Slack and Teams. These serve only their integration endpoints on a separate port, so only that port needs to be reachable from the internet.
  • For internet-exposed webhooks, configure a signature scheme so deliveries are cryptographically verified, and restrict source IPs at your firewall or reverse proxy where practical.

If you must expose the web UI itself to the internet, terminate TLS in one of the supported ways:

A reverse proxy can still be useful for centralized certificate management, a web application firewall, IP allowlists, or other edge policy, but it is not required solely to provide HTTP/2. With either topology, expose only the required port through the host and network firewalls, keep CoderFlow current, and require MFA or SSO for all users.

Limit What a Compromised Container Can Reach​

Task containers execute agent-driven code, including dependencies fetched from package registries. Treat them as semi-trusted: assume one could eventually run hostile code, and limit what that code can reach.

Task containers are not offline sandboxes. Depending on the environment and selected features, they need these outbound flows:

DestinationUsed forTypical TCP port
The effective CoderFlow callback URLFetching Git credentials during repository sync and calling task-scoped CoderFlow APIsThe URL's configured port: normally 3000 for a direct server, or 443/the configured reverse-proxy port
A directly connected IBM iBuild, SSH, source sync, and file deploy22
Profound Logic Remote Access Server (RAS) on a directly connected IBM iSQL8240
Profound UI on a directly connected IBM iInteractive SessionsThe port in the connection's PUI Base URL
Internal Git or other configured connection hostsRepository and environment-specific accessThe port configured for that service, commonly 22 or 443 for Git
Internet services selected by the taskAI providers, Git hosts, package registries, MCP servers, and similar integrationsService-specific, commonly 443

The callback URL is the value CoderFlow hands the container when it starts. Configure site_url or CODERFLOW_INTERNAL_URL so that it names an address the container can resolve and reach. If you later change its protocol, host, or port, relaunch existing task containers because they keep the URL with which they were created.

Docker provides the DOCKER-USER firewall chain for controlling traffic that is forwarded through the host. Rules there are not overwritten by Docker. Docker normally creates a final RETURN rule in that chain; a rule added with iptables -A can land after it and never run. Insert your rules before that return instead. The example below uses explicit positions to keep the rules together and in the required order. Substitute the real addresses and keep only the ports the environment uses:

# Keep reply traffic for permitted outbound connections working. This must
# precede the private-range rejects because replies target the container's
# private bridge address.
sudo iptables -I DOCKER-USER 1 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

# CoderFlow callback through a private reverse proxy
sudo iptables -I DOCKER-USER 2 -p tcp -d 192.0.2.10 --dport 443 -j ACCEPT

# Direct IBM i: SSH/build/sync/deploy and RAS/SQL
sudo iptables -I DOCKER-USER 3 -p tcp -d 192.0.2.20 --dport 22 -j ACCEPT
sudo iptables -I DOCKER-USER 4 -p tcp -d 192.0.2.20 --dport 8240 -j ACCEPT

# Internal HTTPS Git host
sudo iptables -I DOCKER-USER 5 -p tcp -d 192.0.2.30 --dport 443 -j ACCEPT

# Block the cloud metadata service (always safe; prevents credential
# theft from the hosting platform on AWS/Azure/GCP and similar)
sudo iptables -I DOCKER-USER 6 -d 169.254.169.254 -j REJECT

# Block the rest of your internal address space so a compromised
# container cannot scan or pivot into the LAN
sudo iptables -I DOCKER-USER 7 -d 10.0.0.0/8 -j REJECT
sudo iptables -I DOCKER-USER 8 -d 172.16.0.0/12 -j REJECT
sudo iptables -I DOCKER-USER 9 -d 192.168.0.0/16 -j REJECT

This example limits access to private networks; it is not a complete internet-egress allow list. If your policy ends the chain with a default reject or drop, first allow every external service the task requires. Provider, registry, Git, and MCP endpoints can change, so maintain and test that list as part of environment configuration.

Optional CoderFlow-managed Container Network​

Managed networking is off by default on both new and upgraded installations. Enable it in setup.json in your coder-setup directory, then reload the server configuration (or restart the server):

{
"managed_network": {
"enabled": true,
"subnet": "172.30.0.0/24"
}
}

Administrators with settings:manage can also read or replace this configuration through GET /settings/managed-network and PUT /settings/managed-network. The PUT body is the object above (without the managed_network wrapper); changes reload automatically. There is no Settings UI for this option. A successful save validates the configuration; Docker checks the network when creating a container.

When enabled, new task, interactive-session, deployment, documentation-publish, prompt, QA/test, Portal environment-runtime, Codex plan-usage probe and reachability-test containers use the single coderflow-managed bridge. Docker creates it with com.docker.network.bridge.enable_icc=false, which blocks direct traffic between containers on that bridge, including editor and application ports. Published preview ports use host loopback by default; an app-server port exposed with a configured non-loopback bind address can still be reached by containers on the managed network through that host address. Portal desktop deployments retain their dedicated network, and containers deliberately configured without networking remain offline. CoderFlow Link uses Docker exec to bootstrap each container's network broker and accepts this custom bridge; its per-service authorization is unchanged. External QA destinations are still reached through the server proxy.

This is direct container isolation, not an outbound allow list. Containers still need the outbound destinations listed above. Host services and any service intentionally exposed through a proxy need their own access policy. Host firewall rules that accept forwarded traffic before Docker's isolation rules can override bridge isolation; verify both editor and app ports from another container after changing firewall or Docker configuration.

Choose the subnet before enabling. 172.30.0.0/24 is only a default, not a range guaranteed to be free. Choose a canonical private IPv4 CIDR (/8 through /29) that does not overlap your LAN, VPN routes, reachable service networks, existing Docker networks or Docker's planned address pools. Inspect ip route and docker network inspect on the Docker host and check your network inventory. Docker rejects overlap with another Docker network; it cannot detect all remote LAN/VPN conflicts. An overlap can route intended LAN traffic to the bridge.

The network is created once and verified before every new container. A same-named network with different ownership, subnet, driver or isolation options is refused with an error; CoderFlow never modifies or deletes it. Docker-added IPv4-enabled/IPv6-disabled options and a valid MTU are accepted; other additional options are refused. To change the subnet, disable the setting, drain and remove containers using that network, then have the host administrator remove the unused coderflow-managed network. Update the subnet and its firewall rules, and enable the setting again. Do not remove a network used by running or stopped containers you need to preserve.

Existing containers keep their original network and callback URL, even after a restart. Recreate them to apply a network change. To opt out or roll back, set enabled to false; newly created containers again use Docker's default bridge. Recreate managed-network containers to move them back. Disabling does not remove the network or change existing containers. When downgrading the server, disable first and recreate affected containers; the previous build ignores this setting. No data migration is involved in this option. At runtime, an absent/null/false setting, or an absent/false enabled, leaves the default network unchanged even if unused fields are invalid. API writes still validate the entire configuration, and malformed activation values such as "false" are refused.

Docker's host.docker.internal:host-gateway mapping still reaches the host; it may resolve to the default bridge gateway even from the managed bridge. The host firewall sees the managed subnet as the source, so use the rule below. Environments that use a literal host address, such as the default bridge gateway, need checking; enabling a managed network does not rewrite environment setup or frozen container environment variables.

The Callback May Need a Host INPUT Rule​

DOCKER-USER does not govern a packet whose destination is an address owned by the Docker host itself; that packet is delivered through the host's INPUT path. A host firewall that accepts the CoderFlow port only from the LAN can still reject the callback because it sees the Docker bridge address as the source, commonly a 172.x address.

When the effective callback URL terminates on the Docker host, allow the task-container subnet to reach only that callback port in the host firewall. Determine the actual subnet instead of assuming the common default:

docker network inspect bridge --format '{{(index .IPAM.Config 0).Subnet}}'

Then add the equivalent rule using your firewall manager. For example, if the reported subnet is 172.17.0.0/16 and CoderFlow listens directly on port 3000:

sudo iptables -I INPUT -p tcp -s 172.17.0.0/16 --dport 3000 -j ACCEPT

For the managed bridge, inspect its subnet instead:

docker network inspect coderflow-managed --format '{{(index .IPAM.Config 0).Subnet}}'

For the default managed subnet and a callback on port 3000, the equivalent rule is:

sudo iptables -I INPUT -p tcp -s 172.30.0.0/24 --dport 3000 -j ACCEPT

Install the rule for your chosen subnet before starting managed-network tasks. Keep the default-bridge rule while old containers still use that bridge. These are host INPUT rules, not FORWARD rules, and do not open container-to-container ports. Check a new task's callback, Git credential request and repository sync with the rules in place.

If the callback terminates at a reverse proxy on the same host, allow its listen port instead. Do not open the port to all sources merely to make container callbacks work.

Troubleshooting Restricted Egress​

These startup failures commonly indicate that the allow list or host firewall is too strict:

  • repository_sync fails, or the log says the Git credential helper failed to reach /api/git/credentials: the container cannot reach the effective CoderFlow callback URL. Check its DNS, protocol, certificate trust, port, the DOCKER-USER path when routed through another address, and the host INPUT path when it terminates locally.
  • Connection preflight reports a timeout, no route to host, or connection refused: allow the configured destination and feature port. For a direct IBM i, check SSH on 22, RAS on 8240, or the port from the PUI Base URL.
  • Per-task build-library creation fails after repository sync: the container needs SSH access to the configured IBM i on port 22.

After changing rules, launch a new task that exercises every configured connection. Existing containers retain their original callback URL. Also remember:

  • iptables rules do not survive a reboot on their own. Persist them with your distribution's mechanism (for example iptables-persistent on Debian/Ubuntu).

Credentials Inside Task Containers​

Connections marked Available For: Tasks or Deploy are injected into every task or deployment container in their environment, along with Git credentials and AI provider authentication. Any code running in such a container can read them. Plan accordingly:

  • Use Automation-only connections for background jobs. They are read by server-side automations and never injected into any container. This matters most for connections with broad IBM i authority.
  • Scope what you inject. Use IBM i profiles with the least authority the tasks actually need, and Git provider credentials limited to the repositories in the environment. Avoid injecting build or deploy credentials into interactive task environments that do not deploy.
  • Restrict installed SSH keys at the destination. On systems reachable only from a known CoderFlow host, add an OpenSSH from= source restriction to the installed key. CoderFlow does not add one automatically; follow the cautions in SSH Key Management.
  • Prefer separate environments over one broad one. Credentials are injected per environment, so an environment with fewer connections exposes less.

If You Suspect a Container Was Compromised​

  1. Stop the container (Administration → Containers), but keep it for investigation. Do not delete it immediately.
  2. Rotate everything that was reachable from inside it:
    • IBM i credentials (SSH keys and passwords) for the environment's connections
    • Git provider credentials and tokens
    • AI provider API keys and OAuth sessions
    • CoderFlow user passwords and API keys, if the server itself may have been reached
  3. Check other containers and the host for unexpected processes and outbound connections (docker stats and docker top help; sustained maximum CPU in an idle task is a classic sign of a crypto-miner).
  4. Re-verify network exposure from the outside, as described above, before returning to normal operation.

Git SSH Host Keys​

CoderFlow records the first SSH key presented by each Git host and refuses a changed key before authentication. Skill imports and environment repository reads keep these public keys in git-known-hosts in the server data directory (SERVER_DATA_PATH, normally ~/.coder/data). The file uses OpenSSH's known-hosts format, is replaced atomically, and has mode 0600. First connections use StrictHostKeyChecking=accept-new; subsequent connections also restrict negotiation to the recorded key types while preserving the algorithms allowed by the SSH configuration. An existing per-host configuration that explicitly permits legacy RSA signatures continues to work; CoderFlow does not enable those signatures globally. Each Git operation opens a fresh SSH connection.

New task, interactive and deploy containers receive a snapshot of the server's recorded Git keys. Rebuilt base and environment images use that snapshot in ~/.ssh/coderflow_git_known_hosts, record previously unknown Git hosts there, and retain those records across restarts. The initial snapshot takes precedence over keys acquired during an image build, including deploy and test starts without configured repositories. Direct Git invocations also initialize it before connecting. Container records do not update the server's store. Git commands launched later in the container use the same checking through Git's core.sshCommand. IBM i connections, including Profound UI file deployment, continue to use their generated SSH configuration and the accepted keys for the real IBM i host when using CoderFlow Bridge.

Upgrade Behavior​

Server-side Git SSH checks take effect with the server upgrade. No key enrollment or administrator action is required: the first connection records the host's key automatically. HTTPS repositories are unchanged. To apply the container behavior, rebuild the base image and environment images and recreate long-lived containers. Existing images and containers keep their previous behavior until replaced; an upgrade does not discard their state. There is no switch to turn off verification in the managed Git SSH helper.

Replacing a Git Host Key​

A refusal identifies the host, the recorded fingerprint and the presented fingerprint. If SSH cannot retrieve the new key, the presented fingerprint is reported as unavailable and the connection is still refused. Verify the replacement fingerprint with the Git host's administrator through a trusted channel before removing any record.

Stop Git operations and container creation while changing the store. Run these commands as the CoderFlow server account, replacing the example path and host with the values in the refusal. A nonstandard port uses [host]:port as the name.

ssh-keygen -R 'git.example.com' -f '/path/to/server-data/git-known-hosts'
chmod 600 '/path/to/server-data/git-known-hosts'

Retry the server-side repository read to record the replacement, then recreate affected containers so they receive the updated snapshot. For a host used only inside a container, an administrator can remove its stale entry from that container's ~/.ssh/coderflow_git_known_hosts while its Git operations are stopped, then retry. Do not delete unrelated host entries or replace the file with an empty one.

First connections probe the public host key without offering credentials, then save it under a short transaction before opening the authenticated Git connection. Concurrent first connections keep the first saved record. Network handshakes do not hold the store lock or delay other hosts. SSH connection setup has a 10-second timeout, and a first-use probe has an overall 15-second deadline. A busy store transaction refuses after 5 seconds. Normal Git transfers have no additional time limit.

The transaction lock (git-known-hosts.lock.sqlite, mode 0600) releases automatically if its process exits or is killed. SSH and its connection helper also terminate when their supervising Git helper exits. No manual lock-directory removal is needed. A malformed, unreadable or unwritable store refuses the operation; restore the public-key file from a trusted backup or correct its permissions. If the entire store is deleted, CoderFlow recreates an empty file automatically and records hosts again on first use; initialization markers need no cleanup. Deletion removes all remembered keys, so use the per-host replacement procedure above for key changes. Back up git-known-hosts with the data directory. Rolling back the server does not require a data conversion; the previous version ignores this additional file.

Credentials at Rest​

Stored credentials (environment secrets, connection passwords and keys, Git provider and personal secrets, AI provider accounts, and the Slack, Teams, Jira, SMTP and SSO credentials) are encrypted in the server data directory and the setup directory with keys kept in the data directory. API keys and password setup links are stored only as hashes. Anyone who can read the data directory as the server account can still read the keys, so keep that account and its backups private. Back up the keys with the data they protect, and never mix keys and data from different backups: see Backup and Recovery.

Host Sizing and Monitoring​

CoderFlow enables resource limits for runtime containers by default. Configure them under Server Settings → Containers:

  • Leaving CPUs blank gives task and interactive-session containers without a configured cgroup parent a ceiling of half the host's CPU cores, with a minimum of two CPUs capped at the host's core count. Deployment, test, documentation-publish, and prompt containers have no automatic CPU ceiling, nor do task and interactive-session containers with a configured cgroup parent. An explicit CPUs value sets a ceiling for all these container kinds, capped at the host's core count.
  • Leaving Memory blank limits each container to half of the host's memory.
  • Max Processes defaults to 512.

Memory and process-count limits apply to new task, interactive-session, deployment, test, documentation-publish, and prompt containers; automatic CPU ceilings have the exceptions above. CPU shares still control relative CPU priority under contention when there is no per-container CPU ceiling. When using a cgroup parent, configure and verify its aggregate CPU quota on the host. Existing containers keep the limits with which they started, so recreate them after changing the setting. Disabling the limits lets one runaway workload compete with the server and every other container.

Resource limits do not replace host sizing or storage monitoring. Size the host per Server Requirements, watch the Administration → Health panel, and configure automatic Docker storage cleanup separately.

Keep the Server Updated​

Security fixes ship as regular releases. Check Administration → Health → Check for Updates, or enable web-managed updates in Server Settings → Update Management to apply them from the browser. Review the release notes for security-relevant changes.