mirror of
https://github.com/jmcorgan/fips.git
synced 2026-07-30 19:46:15 +00:00
Add Docker sidecar deployment for FIPS
Tailscale-style sidecar pattern: a FIPS container provides mesh networking, and a companion app container shares its network namespace via network_mode: service:fips. Security model: - iptables enforces strict isolation — the app container can only communicate over the FIPS mesh (fd::/8 via fips0) - No IPv4 access: eth0 restricted to FIPS UDP transport (port 2121) - No IPv6 on eth0: ip6tables blocks all eth0 IPv6 traffic - Only fips0 and loopback are reachable from the app container The sidecar accepts peer configuration via environment variables (FIPS_NSEC, FIPS_PEER_NPUB, FIPS_PEER_ADDR), so it can be pointed at any FIPS node without config file generation. Files: - testing/sidecar/: Dockerfile, Dockerfile.app, docker-compose.yml, entrypoint.sh, .env, resolv.conf, scripts/build.sh - testing/sidecar/README.md: security model, quick-start, architecture, DNS resolution, troubleshooting, production considerations - testing/sidecar/scripts/test-sidecar.sh: 3-node chain integration test verifying link establishment, multi-hop connectivity, and network isolation on each app container - .github/workflows/ci.yml: sidecar integration test matrix entry
This commit is contained in:
@@ -178,6 +178,9 @@ jobs:
|
|||||||
- suite: mixed-technology
|
- suite: mixed-technology
|
||||||
type: chaos
|
type: chaos
|
||||||
scenario: mixed-technology
|
scenario: mixed-technology
|
||||||
|
# ── Sidecar deployment ──────────────────────────────────────────
|
||||||
|
- suite: sidecar
|
||||||
|
type: sidecar
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@@ -255,4 +258,25 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: sim-results-${{ matrix.scenario }}
|
name: sim-results-${{ matrix.scenario }}
|
||||||
path: testing/chaos/sim-results/
|
path: testing/chaos/sim-results/
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
|
|
||||||
|
# ── Sidecar deployment ──────────────────────────────────────────────
|
||||||
|
- name: Install binary (sidecar)
|
||||||
|
if: matrix.type == 'sidecar'
|
||||||
|
run: |
|
||||||
|
chmod +x _bin/fips _bin/fipsctl
|
||||||
|
cp _bin/fips testing/sidecar/fips
|
||||||
|
cp _bin/fipsctl testing/sidecar/fipsctl
|
||||||
|
|
||||||
|
- name: Run sidecar integration test
|
||||||
|
if: matrix.type == 'sidecar'
|
||||||
|
run: bash testing/sidecar/scripts/test-sidecar.sh
|
||||||
|
|
||||||
|
- name: Collect logs on failure (sidecar)
|
||||||
|
if: matrix.type == 'sidecar' && failure()
|
||||||
|
run: |
|
||||||
|
for node in a b c; do
|
||||||
|
echo "--- sidecar-${node} logs ---"
|
||||||
|
docker logs "sidecar-${node}-fips-1" 2>&1 || true
|
||||||
|
echo ""
|
||||||
|
done
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# FIPS sidecar default configuration.
|
||||||
|
# Override these values or create a .env.local file.
|
||||||
|
|
||||||
|
# Node identity (hex-encoded secret key).
|
||||||
|
# WARNING: This is a development-only default. In production, use Docker
|
||||||
|
# secrets or a vault — never commit real keys to version control.
|
||||||
|
FIPS_NSEC=e752b92aed3ac1595807f5d0eb5125589fbec0a2cfd3a2948d87ea076557deeb
|
||||||
|
|
||||||
|
# Peer configuration (leave empty for standalone operation)
|
||||||
|
FIPS_PEER_NPUB=
|
||||||
|
FIPS_PEER_ADDR=
|
||||||
|
FIPS_PEER_ALIAS=peer
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fips
|
||||||
|
fipsctl
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
FROM debian:bookworm-slim
|
||||||
|
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y --no-install-recommends \
|
||||||
|
iproute2 iputils-ping dnsutils dnsmasq iptables \
|
||||||
|
openssh-client openssh-server python3 \
|
||||||
|
tcpdump netcat-openbsd curl iperf3 && \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Setup SSH server with no authentication (test only!)
|
||||||
|
RUN mkdir -p /var/run/sshd && \
|
||||||
|
ssh-keygen -A && \
|
||||||
|
sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config && \
|
||||||
|
sed -i 's/#PermitEmptyPasswords no/PermitEmptyPasswords yes/' /etc/ssh/sshd_config && \
|
||||||
|
sed -i 's/UsePAM yes/UsePAM no/' /etc/ssh/sshd_config && \
|
||||||
|
passwd -d root
|
||||||
|
|
||||||
|
# dnsmasq: forward .fips to FIPS daemon, everything else to Docker DNS
|
||||||
|
RUN printf '%s\n' \
|
||||||
|
'port=53' \
|
||||||
|
'listen-address=127.0.0.1' \
|
||||||
|
'bind-interfaces' \
|
||||||
|
'server=/fips/127.0.0.1#5354' \
|
||||||
|
'server=127.0.0.11' \
|
||||||
|
'no-resolv' \
|
||||||
|
>> /etc/dnsmasq.conf
|
||||||
|
|
||||||
|
COPY fips fipsctl /usr/local/bin/
|
||||||
|
RUN chmod +x /usr/local/bin/fips /usr/local/bin/fipsctl
|
||||||
|
|
||||||
|
COPY entrypoint.sh /entrypoint.sh
|
||||||
|
|
||||||
|
ENTRYPOINT ["/entrypoint.sh"]
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
FROM debian:bookworm-slim
|
||||||
|
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y --no-install-recommends \
|
||||||
|
iproute2 iputils-ping dnsutils iptables \
|
||||||
|
openssh-client openssh-server python3 \
|
||||||
|
tcpdump netcat-openbsd curl iperf3 && \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Setup SSH server with no authentication (test only!)
|
||||||
|
RUN mkdir -p /var/run/sshd && \
|
||||||
|
ssh-keygen -A && \
|
||||||
|
sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config && \
|
||||||
|
sed -i 's/#PermitEmptyPasswords no/PermitEmptyPasswords yes/' /etc/ssh/sshd_config && \
|
||||||
|
sed -i 's/UsePAM yes/UsePAM no/' /etc/ssh/sshd_config && \
|
||||||
|
passwd -d root
|
||||||
|
|
||||||
|
CMD ["sleep", "infinity"]
|
||||||
@@ -0,0 +1,236 @@
|
|||||||
|
# FIPS Sidecar
|
||||||
|
|
||||||
|
Run FIPS as a network sidecar container, providing mesh-only network
|
||||||
|
access to a companion application. The app container shares the FIPS
|
||||||
|
container's network namespace and is isolated from the host network by
|
||||||
|
iptables rules — it can only communicate over the FIPS mesh via `fips0`.
|
||||||
|
|
||||||
|
This is the recommended deployment pattern when connecting to an untrusted
|
||||||
|
public FIPS mesh: the application never touches the underlying transport
|
||||||
|
network, so it cannot leak traffic outside the mesh or be reached by
|
||||||
|
non-mesh peers.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd testing/sidecar
|
||||||
|
./scripts/build.sh
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
# Verify the sidecar is running:
|
||||||
|
docker exec fips-sidecar fipsctl show status
|
||||||
|
|
||||||
|
# Verify the app container can see the FIPS interface:
|
||||||
|
docker exec fips-app ip addr show fips0
|
||||||
|
```
|
||||||
|
|
||||||
|
With the default `.env`, FIPS starts with no peers. See
|
||||||
|
[Run with peers](#run-with-peers) to connect to an existing mesh.
|
||||||
|
|
||||||
|
## Security Model
|
||||||
|
|
||||||
|
The sidecar pattern enforces strict network isolation on the app container:
|
||||||
|
|
||||||
|
- **No IPv4 access**: iptables blocks all eth0 traffic except FIPS UDP
|
||||||
|
transport (port 2121). The app container cannot reach the Docker bridge,
|
||||||
|
the host network, or any IPv4 address.
|
||||||
|
- **No IPv6 on eth0**: ip6tables blocks all IPv6 traffic on eth0. The app
|
||||||
|
container cannot use link-local or any Docker-assigned IPv6 addresses.
|
||||||
|
- **FIPS mesh only**: The only routable network path is through `fips0`
|
||||||
|
(`fd::/8`). All application traffic traverses the FIPS mesh with
|
||||||
|
end-to-end encryption.
|
||||||
|
- **Loopback allowed**: `lo` is unrestricted for inter-process communication
|
||||||
|
within the shared namespace.
|
||||||
|
|
||||||
|
This means the app container treats the FIPS mesh as its sole network. Even
|
||||||
|
if the application is compromised, it cannot bypass the mesh or communicate
|
||||||
|
with the transport layer directly.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```text
|
||||||
|
┌───────────────────────────────────────────────────┐
|
||||||
|
│ Shared network namespace │
|
||||||
|
│ │
|
||||||
|
│ ┌───────────────┐ ┌──────────────────────────┐ │
|
||||||
|
│ │ fips-sidecar │ │ fips-app │ │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ │ fips daemon │ │ your workload │ │
|
||||||
|
│ │ fipsctl │ │ │ │
|
||||||
|
│ │ dnsmasq │ │ │ │
|
||||||
|
│ └───────────────┘ └──────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ Interfaces: │
|
||||||
|
│ lo — loopback (unrestricted) │
|
||||||
|
│ eth0 — Docker bridge (iptables: FIPS only) │
|
||||||
|
│ fips0 — FIPS TUN (fd::/8, unrestricted) │
|
||||||
|
└───────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
The FIPS sidecar owns the network namespace and creates the `fips0` TUN
|
||||||
|
interface. The app container joins via `network_mode: service:fips` and
|
||||||
|
sees the same interfaces. The entrypoint script applies iptables rules
|
||||||
|
before launching the FIPS daemon:
|
||||||
|
|
||||||
|
**IPv4 rules** (iptables):
|
||||||
|
|
||||||
|
- ACCEPT on `lo` (both directions)
|
||||||
|
- ACCEPT UDP sport/dport 2121 on `eth0` (FIPS transport)
|
||||||
|
- DROP everything else on `eth0`
|
||||||
|
|
||||||
|
**IPv6 rules** (ip6tables):
|
||||||
|
|
||||||
|
- ACCEPT on `lo` (both directions)
|
||||||
|
- ACCEPT on `fips0` (both directions)
|
||||||
|
- DROP everything on `eth0`
|
||||||
|
|
||||||
|
### DNS Resolution
|
||||||
|
|
||||||
|
DNS inside the container is handled by dnsmasq (127.0.0.1:53):
|
||||||
|
|
||||||
|
- `.fips` queries are forwarded to the FIPS daemon's built-in DNS resolver
|
||||||
|
(127.0.0.1:5354), which resolves npub-based names to `fd::/8` addresses
|
||||||
|
- All other queries are forwarded to Docker's embedded DNS (127.0.0.11)
|
||||||
|
|
||||||
|
The `resolv.conf` mount points the container's resolver at 127.0.0.1,
|
||||||
|
where dnsmasq handles the routing.
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd testing/sidecar
|
||||||
|
./scripts/build.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
This compiles FIPS for Linux, copies the binaries into the Docker context,
|
||||||
|
and builds the sidecar and app images. Cross-compilation from macOS is
|
||||||
|
supported via `cargo-zigbuild`.
|
||||||
|
|
||||||
|
## Run with Peers
|
||||||
|
|
||||||
|
To connect the sidecar to an existing mesh, provide the peer's npub and
|
||||||
|
transport address:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
FIPS_PEER_NPUB=npub1... \
|
||||||
|
FIPS_PEER_ADDR=203.0.113.10:2121 \
|
||||||
|
FIPS_PEER_ALIAS=gateway \
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify the peer link:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec fips-sidecar fipsctl show peers
|
||||||
|
docker exec fips-sidecar fipsctl show links
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verify Connectivity and Isolation
|
||||||
|
|
||||||
|
From the app container:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Ping a mesh node by npub (resolves via .fips DNS):
|
||||||
|
docker exec fips-app ping6 -c3 npub1sjlh2c3x9w7kjsqg2ay080n2lff2uvt325vpan33ke34rn8l5jcqawh57m.fips
|
||||||
|
|
||||||
|
# Fetch a web page from a mesh node over FIPS:
|
||||||
|
docker exec fips-app curl -6 "http://[fd69:e08d:65cc:3a6b:9c2c:2ac4:bd40:5e4b]:8000/"
|
||||||
|
|
||||||
|
# Docker bridge is blocked — this should fail:
|
||||||
|
docker exec fips-app ping -c1 -W2 172.20.0.13
|
||||||
|
|
||||||
|
# Loopback is allowed:
|
||||||
|
docker exec fips-app ping -c1 127.0.0.1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `FIPS_NSEC` | *(required)* | Node secret key (hex or nsec1 bech32) |
|
||||||
|
| `FIPS_PEER_NPUB` | *(empty)* | Peer's npub to connect to |
|
||||||
|
| `FIPS_PEER_ADDR` | *(empty)* | Peer's transport address (e.g. `203.0.113.10:2121`) |
|
||||||
|
| `FIPS_PEER_ALIAS` | `peer` | Human-readable peer name |
|
||||||
|
| `FIPS_UDP_BIND` | `0.0.0.0:2121` | UDP transport bind address |
|
||||||
|
| `FIPS_TUN_MTU` | `1280` | TUN interface MTU |
|
||||||
|
| `FIPS_NETWORK` | `fips-sidecar-net` | Docker network name (set to join external network) |
|
||||||
|
| `FIPS_SUBNET` | `172.20.1.0/24` | Docker network subnet |
|
||||||
|
| `FIPS_IPV4` | `172.20.1.20` | Sidecar's IPv4 address on the Docker network |
|
||||||
|
| `RUST_LOG` | `info` | FIPS log level |
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
**`FIPS_NSEC is required`** — The `FIPS_NSEC` environment variable is not
|
||||||
|
set. Either add it to `.env` or pass it on the command line. Generate a
|
||||||
|
random key with: `openssl rand -hex 32`
|
||||||
|
|
||||||
|
**`fips0` interface not appearing** — The FIPS daemon needs `/dev/net/tun`
|
||||||
|
and `NET_ADMIN` capability. Check that the compose file includes both:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
cap_add:
|
||||||
|
- NET_ADMIN
|
||||||
|
devices:
|
||||||
|
- /dev/net/tun:/dev/net/tun
|
||||||
|
```
|
||||||
|
|
||||||
|
**No peer connection established** — Verify the peer address is reachable
|
||||||
|
from the sidecar container (`docker exec fips-sidecar ping -c1 <peer-ip>`).
|
||||||
|
If joining an external Docker network, ensure `FIPS_NETWORK`, `FIPS_SUBNET`,
|
||||||
|
and `FIPS_IPV4` match the target network. Check logs with
|
||||||
|
`docker logs fips-sidecar`.
|
||||||
|
|
||||||
|
**DNS not resolving `.fips` names** — Verify dnsmasq is running:
|
||||||
|
`docker exec fips-sidecar pgrep dnsmasq`. Check that `resolv.conf` is
|
||||||
|
mounted (should contain `nameserver 127.0.0.1`). Verify the FIPS DNS
|
||||||
|
resolver is listening: `docker exec fips-sidecar dig @127.0.0.1 -p 5354 <npub>.fips AAAA`.
|
||||||
|
|
||||||
|
**iptables errors in entrypoint** — The sidecar container requires
|
||||||
|
`NET_ADMIN` capability for iptables. Without it, the isolation rules
|
||||||
|
cannot be applied and the entrypoint will fail.
|
||||||
|
|
||||||
|
## Production Considerations
|
||||||
|
|
||||||
|
**Secrets management**: The default `.env` contains a hardcoded nsec for
|
||||||
|
development. In production, use Docker secrets, a vault, or inject the key
|
||||||
|
via a secure CI/CD pipeline. Never commit production keys to version control.
|
||||||
|
|
||||||
|
**Logging**: Set `RUST_LOG` to control log verbosity (`debug`, `info`,
|
||||||
|
`warn`, `error`). For production, configure the Docker logging driver with
|
||||||
|
size limits:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
logging:
|
||||||
|
driver: json-file
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "3"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Resource limits**: Add memory and CPU constraints in the compose file:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 256M
|
||||||
|
cpus: "0.5"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Multiple peers**: The entrypoint supports a single peer via environment
|
||||||
|
variables. For multiple peers, mount a custom `fips.yaml` directly:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
volumes:
|
||||||
|
- ./my-fips.yaml:/etc/fips/fips.yaml:ro
|
||||||
|
```
|
||||||
|
|
||||||
|
**Health checks**: Add a Docker health check using `fipsctl`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "fipsctl", "show", "status"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
```
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# Override: use a pre-existing external network instead of creating one.
|
||||||
|
# Used by test-sidecar.sh for multi-instance testing on a shared network.
|
||||||
|
networks:
|
||||||
|
fips-net:
|
||||||
|
external: true
|
||||||
|
name: ${FIPS_NETWORK:-fips-sidecar-net}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
networks:
|
||||||
|
fips-net:
|
||||||
|
name: ${FIPS_NETWORK:-fips-sidecar-net}
|
||||||
|
driver: bridge
|
||||||
|
ipam:
|
||||||
|
config:
|
||||||
|
- subnet: ${FIPS_SUBNET:-172.20.1.0/24}
|
||||||
|
|
||||||
|
services:
|
||||||
|
fips:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
hostname: fips-sidecar
|
||||||
|
cap_add:
|
||||||
|
- NET_ADMIN
|
||||||
|
devices:
|
||||||
|
- /dev/net/tun:/dev/net/tun
|
||||||
|
sysctls:
|
||||||
|
- net.ipv6.conf.all.disable_ipv6=0
|
||||||
|
restart: "no"
|
||||||
|
environment:
|
||||||
|
- RUST_LOG=${RUST_LOG:-info}
|
||||||
|
- FIPS_NSEC=${FIPS_NSEC}
|
||||||
|
- FIPS_PEER_NPUB=${FIPS_PEER_NPUB:-}
|
||||||
|
- FIPS_PEER_ADDR=${FIPS_PEER_ADDR:-}
|
||||||
|
- FIPS_PEER_ALIAS=${FIPS_PEER_ALIAS:-peer}
|
||||||
|
- FIPS_UDP_BIND=${FIPS_UDP_BIND:-0.0.0.0:2121}
|
||||||
|
- FIPS_TUN_MTU=${FIPS_TUN_MTU:-1280}
|
||||||
|
volumes:
|
||||||
|
- ./resolv.conf:/etc/resolv.conf:ro
|
||||||
|
networks:
|
||||||
|
fips-net:
|
||||||
|
ipv4_address: ${FIPS_IPV4:-172.20.1.20}
|
||||||
|
|
||||||
|
app:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.app
|
||||||
|
network_mode: "service:fips"
|
||||||
|
depends_on:
|
||||||
|
- fips
|
||||||
|
volumes:
|
||||||
|
- ./resolv.conf:/etc/resolv.conf:ro
|
||||||
|
command: ["sleep", "infinity"]
|
||||||
Executable
+80
@@ -0,0 +1,80 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# FIPS sidecar entrypoint: generate config, apply iptables isolation, launch FIPS.
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# --- Generate FIPS config from environment variables ---
|
||||||
|
|
||||||
|
FIPS_NSEC="${FIPS_NSEC:?FIPS_NSEC is required}"
|
||||||
|
FIPS_UDP_BIND="${FIPS_UDP_BIND:-0.0.0.0:2121}"
|
||||||
|
FIPS_TUN_MTU="${FIPS_TUN_MTU:-1280}"
|
||||||
|
|
||||||
|
mkdir -p /etc/fips
|
||||||
|
|
||||||
|
# Build peers section
|
||||||
|
PEERS_SECTION=""
|
||||||
|
if [ -n "$FIPS_PEER_NPUB" ] && [ -n "$FIPS_PEER_ADDR" ]; then
|
||||||
|
FIPS_PEER_ALIAS="${FIPS_PEER_ALIAS:-peer}"
|
||||||
|
PEERS_SECTION=" - npub: \"${FIPS_PEER_NPUB}\"
|
||||||
|
alias: \"${FIPS_PEER_ALIAS}\"
|
||||||
|
addresses:
|
||||||
|
- transport: udp
|
||||||
|
addr: \"${FIPS_PEER_ADDR}\"
|
||||||
|
connect_policy: auto_connect"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat > /etc/fips/fips.yaml <<EOF
|
||||||
|
node:
|
||||||
|
identity:
|
||||||
|
nsec: "${FIPS_NSEC}"
|
||||||
|
|
||||||
|
tun:
|
||||||
|
enabled: true
|
||||||
|
name: fips0
|
||||||
|
mtu: ${FIPS_TUN_MTU}
|
||||||
|
|
||||||
|
dns:
|
||||||
|
enabled: true
|
||||||
|
bind_addr: "127.0.0.1"
|
||||||
|
|
||||||
|
transports:
|
||||||
|
udp:
|
||||||
|
bind_addr: "${FIPS_UDP_BIND}"
|
||||||
|
mtu: 1472
|
||||||
|
|
||||||
|
peers:
|
||||||
|
${PEERS_SECTION:- []}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "Generated /etc/fips/fips.yaml"
|
||||||
|
|
||||||
|
# --- Apply iptables rules for strict network isolation ---
|
||||||
|
#
|
||||||
|
# Goal: only FIPS UDP transport (port 2121) may use eth0.
|
||||||
|
# All other eth0 traffic is dropped. fips0 and loopback are unrestricted.
|
||||||
|
# This ensures the app container (sharing this network namespace) can only
|
||||||
|
# communicate over the FIPS mesh.
|
||||||
|
|
||||||
|
# IPv4: allow only FIPS transport on eth0
|
||||||
|
iptables -A OUTPUT -o lo -j ACCEPT
|
||||||
|
iptables -A INPUT -i lo -j ACCEPT
|
||||||
|
iptables -A OUTPUT -o eth0 -p udp --dport 2121 -j ACCEPT
|
||||||
|
iptables -A OUTPUT -o eth0 -p udp --sport 2121 -j ACCEPT
|
||||||
|
iptables -A INPUT -i eth0 -p udp --dport 2121 -j ACCEPT
|
||||||
|
iptables -A INPUT -i eth0 -p udp --sport 2121 -j ACCEPT
|
||||||
|
iptables -A OUTPUT -o eth0 -j DROP
|
||||||
|
iptables -A INPUT -i eth0 -j DROP
|
||||||
|
|
||||||
|
# IPv6: allow fips0 and loopback, block eth0
|
||||||
|
ip6tables -A OUTPUT -o lo -j ACCEPT
|
||||||
|
ip6tables -A INPUT -i lo -j ACCEPT
|
||||||
|
ip6tables -A OUTPUT -o fips0 -j ACCEPT
|
||||||
|
ip6tables -A INPUT -i fips0 -j ACCEPT
|
||||||
|
ip6tables -A OUTPUT -o eth0 -j DROP
|
||||||
|
ip6tables -A INPUT -i eth0 -j DROP
|
||||||
|
|
||||||
|
echo "iptables isolation rules applied"
|
||||||
|
|
||||||
|
# --- Start dnsmasq and launch FIPS ---
|
||||||
|
|
||||||
|
dnsmasq
|
||||||
|
exec fips --config /etc/fips/fips.yaml
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
nameserver 127.0.0.1
|
||||||
Executable
+55
@@ -0,0 +1,55 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Build the FIPS binary and Docker sidecar image.
|
||||||
|
# Usage: ./build.sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
DOCKER_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
|
||||||
|
# Find project root (directory containing Cargo.toml)
|
||||||
|
PROJECT_ROOT="$(cd "$DOCKER_DIR/../.." && pwd)"
|
||||||
|
if [ ! -f "$PROJECT_ROOT/Cargo.toml" ]; then
|
||||||
|
echo "Error: Cannot find Cargo.toml at $PROJECT_ROOT" >&2
|
||||||
|
echo "Expected layout: <project-root>/testing/sidecar/scripts/build.sh" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Detect host OS
|
||||||
|
UNAME_S=$(uname -s)
|
||||||
|
CARGO_TARGET="x86_64-unknown-linux-musl"
|
||||||
|
|
||||||
|
if [ "$UNAME_S" = "Darwin" ]; then
|
||||||
|
echo "Detected macOS host — using cross-compilation for Linux..."
|
||||||
|
|
||||||
|
if ! command -v cargo-zigbuild &> /dev/null; then
|
||||||
|
echo "Error: cargo-zigbuild not found." >&2
|
||||||
|
echo "Please install it: cargo install cargo-zigbuild" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! rustup target list --installed | grep -q "$CARGO_TARGET"; then
|
||||||
|
echo "Installing Rust target $CARGO_TARGET..."
|
||||||
|
rustup target add "$CARGO_TARGET"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Building FIPS for Linux (release) using cargo-zigbuild..."
|
||||||
|
cargo zigbuild --release --target "$CARGO_TARGET" --manifest-path="$PROJECT_ROOT/Cargo.toml"
|
||||||
|
|
||||||
|
echo "Copying binaries to docker context..."
|
||||||
|
cp "$PROJECT_ROOT/target/$CARGO_TARGET/release/fips" "$DOCKER_DIR/fips"
|
||||||
|
cp "$PROJECT_ROOT/target/$CARGO_TARGET/release/fipsctl" "$DOCKER_DIR/fipsctl"
|
||||||
|
else
|
||||||
|
echo "Building FIPS (release)..."
|
||||||
|
cargo build --release --manifest-path="$PROJECT_ROOT/Cargo.toml"
|
||||||
|
|
||||||
|
echo "Copying binaries to docker context..."
|
||||||
|
cp "$PROJECT_ROOT/target/release/fips" "$DOCKER_DIR/fips"
|
||||||
|
cp "$PROJECT_ROOT/target/release/fipsctl" "$DOCKER_DIR/fipsctl"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Done. Binaries at $DOCKER_DIR/{fips,fipsctl}"
|
||||||
|
echo ""
|
||||||
|
echo "Building Docker image..."
|
||||||
|
docker compose -f "$DOCKER_DIR/docker-compose.yml" build
|
||||||
|
echo ""
|
||||||
|
echo "Ready: cd testing/sidecar && docker compose up -d"
|
||||||
Executable
+230
@@ -0,0 +1,230 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Integration test for the FIPS sidecar deployment.
|
||||||
|
#
|
||||||
|
# Starts a 3-node chain (A—B—C) using standalone sidecar instances,
|
||||||
|
# verifies link establishment, multi-hop connectivity, and network
|
||||||
|
# isolation on each app container.
|
||||||
|
#
|
||||||
|
# Usage: ./test-sidecar.sh [--skip-build]
|
||||||
|
#
|
||||||
|
# Exit codes:
|
||||||
|
# 0 — all tests passed
|
||||||
|
# 1 — test failure
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
SIDECAR_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
|
||||||
|
# Deterministic keys derived from: derive-keys.py sidecar-test node-{a,b,c}
|
||||||
|
NODE_A_NSEC="9e688d0879fa9cd025fea0487ac23495080e3de626070fdb9b78dc1f619dd453"
|
||||||
|
NODE_A_NPUB="npub1jvren5hnege54lu3p2nzqacctmulqvkgp68yvfuj5jme5dtgnhxsdh6788"
|
||||||
|
NODE_B_NSEC="3e4e10614c0490575fa5e994524ff3f4deaac2f20db189cc9c9a79da0d90f17a"
|
||||||
|
NODE_B_NPUB="npub15h7z0ljzudqe9pgwx99cjsz2c0ennuyvkcc8zvtk3lg97xwzex9ska6g4y"
|
||||||
|
NODE_C_NSEC="15148ed0131f7da43fd13e369dfedede14fb64698f3756636b569c3a3e87438f"
|
||||||
|
NODE_C_NPUB="npub1zhezcykd0e34z4fxtranl45jaasgnlxv0kjqwlq2v56ggssn0w4qelcrvr"
|
||||||
|
|
||||||
|
NETWORK_NAME="fips-sidecar-test"
|
||||||
|
SUBNET="172.20.2.0/24"
|
||||||
|
NODE_A_IP="172.20.2.10"
|
||||||
|
NODE_B_IP="172.20.2.11"
|
||||||
|
NODE_C_IP="172.20.2.12"
|
||||||
|
|
||||||
|
CONVERGE_TIMEOUT=30
|
||||||
|
PASSED=0
|
||||||
|
FAILED=0
|
||||||
|
|
||||||
|
# Compose file paths
|
||||||
|
COMPOSE_BASE="-f $SIDECAR_DIR/docker-compose.yml"
|
||||||
|
COMPOSE_EXT="$COMPOSE_BASE -f $SIDECAR_DIR/docker-compose.external-net.yml"
|
||||||
|
|
||||||
|
# ── Helpers ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
log() { echo "=== $*"; }
|
||||||
|
pass() { echo " PASS: $*"; PASSED=$((PASSED + 1)); }
|
||||||
|
fail() { echo " FAIL: $*"; FAILED=$((FAILED + 1)); }
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
log "Cleaning up..."
|
||||||
|
# Tear down B and C first (they reference A's network as external)
|
||||||
|
docker compose $COMPOSE_EXT -p sidecar-c down --volumes --remove-orphans 2>/dev/null || true
|
||||||
|
docker compose $COMPOSE_EXT -p sidecar-b down --volumes --remove-orphans 2>/dev/null || true
|
||||||
|
# Tear down A last (it owns the network)
|
||||||
|
docker compose $COMPOSE_BASE -p sidecar-a down --volumes --remove-orphans 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
# Always clean up on exit
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
# ── Build ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
if [[ "${1:-}" != "--skip-build" ]]; then
|
||||||
|
log "Building sidecar images..."
|
||||||
|
docker compose $COMPOSE_BASE build
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Start nodes ────────────────────────────────────────────────────────────
|
||||||
|
#
|
||||||
|
# Chain topology: A — B — C
|
||||||
|
# node-a: no outbound peers (accepts inbound from B)
|
||||||
|
# node-b: peers with A (middle node, transit router)
|
||||||
|
# node-c: peers with B (end node)
|
||||||
|
|
||||||
|
log "Starting node-a (no peers, creates network)..."
|
||||||
|
FIPS_NSEC="$NODE_A_NSEC" \
|
||||||
|
FIPS_NETWORK="$NETWORK_NAME" \
|
||||||
|
FIPS_SUBNET="$SUBNET" \
|
||||||
|
FIPS_IPV4="$NODE_A_IP" \
|
||||||
|
docker compose $COMPOSE_BASE -p sidecar-a up -d
|
||||||
|
|
||||||
|
log "Starting node-b (peers with node-a, joins external network)..."
|
||||||
|
FIPS_NSEC="$NODE_B_NSEC" \
|
||||||
|
FIPS_PEER_NPUB="$NODE_A_NPUB" \
|
||||||
|
FIPS_PEER_ADDR="${NODE_A_IP}:2121" \
|
||||||
|
FIPS_PEER_ALIAS="node-a" \
|
||||||
|
FIPS_NETWORK="$NETWORK_NAME" \
|
||||||
|
FIPS_SUBNET="$SUBNET" \
|
||||||
|
FIPS_IPV4="$NODE_B_IP" \
|
||||||
|
docker compose $COMPOSE_EXT -p sidecar-b up -d
|
||||||
|
|
||||||
|
log "Starting node-c (peers with node-b, joins external network)..."
|
||||||
|
FIPS_NSEC="$NODE_C_NSEC" \
|
||||||
|
FIPS_PEER_NPUB="$NODE_B_NPUB" \
|
||||||
|
FIPS_PEER_ADDR="${NODE_B_IP}:2121" \
|
||||||
|
FIPS_PEER_ALIAS="node-b" \
|
||||||
|
FIPS_NETWORK="$NETWORK_NAME" \
|
||||||
|
FIPS_SUBNET="$SUBNET" \
|
||||||
|
FIPS_IPV4="$NODE_C_IP" \
|
||||||
|
docker compose $COMPOSE_EXT -p sidecar-c up -d
|
||||||
|
|
||||||
|
# ── Wait for convergence ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
log "Waiting for link establishment (up to ${CONVERGE_TIMEOUT}s)..."
|
||||||
|
|
||||||
|
converged=false
|
||||||
|
for i in $(seq 1 "$CONVERGE_TIMEOUT"); do
|
||||||
|
# node-b should have 2 links (A and C)
|
||||||
|
link_count=$(docker exec sidecar-b-fips-1 fipsctl show links 2>/dev/null \
|
||||||
|
| grep -c '"state": "connected"' || true)
|
||||||
|
if [ "$link_count" -ge 2 ]; then
|
||||||
|
converged=true
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$converged" = true ]; then
|
||||||
|
log "Links established after ${i}s"
|
||||||
|
else
|
||||||
|
log "TIMEOUT: links did not converge in ${CONVERGE_TIMEOUT}s"
|
||||||
|
log "node-a links:"
|
||||||
|
docker exec sidecar-a-fips-1 fipsctl show links 2>&1 || true
|
||||||
|
log "node-b links:"
|
||||||
|
docker exec sidecar-b-fips-1 fipsctl show links 2>&1 || true
|
||||||
|
log "node-c links:"
|
||||||
|
docker exec sidecar-c-fips-1 fipsctl show links 2>&1 || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Allow a few more seconds for tree convergence and coordinate propagation
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
# ── Link verification ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
log "Verifying link counts..."
|
||||||
|
|
||||||
|
a_links=$(docker exec sidecar-a-fips-1 fipsctl show links 2>/dev/null \
|
||||||
|
| grep -c '"state": "connected"' || true)
|
||||||
|
b_links=$(docker exec sidecar-b-fips-1 fipsctl show links 2>/dev/null \
|
||||||
|
| grep -c '"state": "connected"' || true)
|
||||||
|
c_links=$(docker exec sidecar-c-fips-1 fipsctl show links 2>/dev/null \
|
||||||
|
| grep -c '"state": "connected"' || true)
|
||||||
|
|
||||||
|
[ "$a_links" -ge 1 ] && pass "node-a has $a_links link(s)" || fail "node-a has $a_links links (expected >= 1)"
|
||||||
|
[ "$b_links" -ge 2 ] && pass "node-b has $b_links link(s)" || fail "node-b has $b_links links (expected >= 2)"
|
||||||
|
[ "$c_links" -ge 1 ] && pass "node-c has $c_links link(s)" || fail "node-c has $c_links links (expected >= 1)"
|
||||||
|
|
||||||
|
# ── Direct connectivity (adjacent nodes) ──────────────────────────────────
|
||||||
|
|
||||||
|
log "Testing direct connectivity (B app → A via fips0)..."
|
||||||
|
|
||||||
|
if docker exec sidecar-b-app-1 ping6 -c2 -W5 "${NODE_A_NPUB}.fips" >/dev/null 2>&1; then
|
||||||
|
pass "node-b app can ping node-a via fips0"
|
||||||
|
else
|
||||||
|
fail "node-b app cannot ping node-a via fips0"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Multi-hop connectivity (C → A through B) ─────────────────────────────
|
||||||
|
|
||||||
|
log "Testing multi-hop connectivity (C app → A via fips0, through B)..."
|
||||||
|
|
||||||
|
if docker exec sidecar-c-app-1 ping6 -c2 -W10 "${NODE_A_NPUB}.fips" >/dev/null 2>&1; then
|
||||||
|
pass "node-c app can ping node-a via fips0 (multi-hop through B)"
|
||||||
|
else
|
||||||
|
fail "node-c app cannot ping node-a via fips0 (multi-hop through B)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Reverse direction (A → C through B) ──────────────────────────────────
|
||||||
|
|
||||||
|
log "Testing reverse multi-hop (A app → C via fips0, through B)..."
|
||||||
|
|
||||||
|
if docker exec sidecar-a-app-1 ping6 -c2 -W10 "${NODE_C_NPUB}.fips" >/dev/null 2>&1; then
|
||||||
|
pass "node-a app can ping node-c via fips0 (multi-hop through B)"
|
||||||
|
else
|
||||||
|
fail "node-a app cannot ping node-c via fips0 (multi-hop through B)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Network isolation verification ────────────────────────────────────────
|
||||||
|
#
|
||||||
|
# This is the critical security assertion: app containers must NOT be able
|
||||||
|
# to reach anything outside the FIPS mesh.
|
||||||
|
|
||||||
|
log "Verifying network isolation on app containers..."
|
||||||
|
|
||||||
|
for node in a b c; do
|
||||||
|
container="sidecar-${node}-app-1"
|
||||||
|
# Pick a peer IP that isn't this node's own address
|
||||||
|
case $node in
|
||||||
|
a) peer_ip="$NODE_B_IP" ;;
|
||||||
|
b) peer_ip="$NODE_C_IP" ;;
|
||||||
|
c) peer_ip="$NODE_A_IP" ;;
|
||||||
|
esac
|
||||||
|
log " Checking $container..."
|
||||||
|
|
||||||
|
# IPv4 gateway should be unreachable (iptables DROP on eth0)
|
||||||
|
if docker exec "$container" ping -c1 -W2 172.20.2.1 >/dev/null 2>&1; then
|
||||||
|
fail "$container can reach IPv4 gateway (isolation broken!)"
|
||||||
|
else
|
||||||
|
pass "$container cannot reach IPv4 gateway (IPv4 blocked)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# IPv4 peer should be unreachable (iptables DROP on eth0)
|
||||||
|
if docker exec "$container" ping -c1 -W2 "$peer_ip" >/dev/null 2>&1; then
|
||||||
|
fail "$container can reach peer IPv4 (isolation broken!)"
|
||||||
|
else
|
||||||
|
pass "$container cannot reach peer IPv4 (IPv4 blocked)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Loopback should work
|
||||||
|
if docker exec "$container" ping -c1 -W2 127.0.0.1 >/dev/null 2>&1; then
|
||||||
|
pass "$container can reach loopback (expected)"
|
||||||
|
else
|
||||||
|
fail "$container cannot reach loopback"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# ── Summary ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
log "Results: $PASSED passed, $FAILED failed"
|
||||||
|
|
||||||
|
if [ "$FAILED" -gt 0 ]; then
|
||||||
|
log "Dumping logs for failed run..."
|
||||||
|
for node in a b c; do
|
||||||
|
echo "--- sidecar-${node} logs ---"
|
||||||
|
docker logs "sidecar-${node}-fips-1" 2>&1 | tail -30
|
||||||
|
echo ""
|
||||||
|
done
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "All tests passed."
|
||||||
Reference in New Issue
Block a user