Skip to content
Merged
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
25 changes: 19 additions & 6 deletions .github/workflows/build-ova.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ on:
gateway_tag:
description: "defguard gateway image tag"
required: true
publish_latest:
description: "Also overwrite defguard-latest.ova (off = test build)"
type: boolean
default: false

jobs:
build:
Expand Down Expand Up @@ -62,9 +66,9 @@ jobs:
env:
PACKER_LOG: 1
run: |
CORE_TAG="${{ github.event.inputs.core_tag }}"
PROXY_TAG="${{ github.event.inputs.proxy_tag }}"
GATEWAY_TAG="${{ github.event.inputs.gateway_tag }}"
CORE_TAG="${{ github.event.inputs.core_tag || '2' }}"
PROXY_TAG="${{ github.event.inputs.proxy_tag || '2' }}"
GATEWAY_TAG="${{ github.event.inputs.gateway_tag || '2' }}"
packer build \
-var "iso_url=file://$PWD/ubuntu-24.04.4-live-server-amd64.iso" \
-var "core_tag=${CORE_TAG}" \
Expand All @@ -77,14 +81,23 @@ jobs:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: eu-central-1
CORE_TAG: ${{ github.event.inputs.core_tag }}
PROXY_TAG: ${{ github.event.inputs.proxy_tag }}
GATEWAY_TAG: ${{ github.event.inputs.gateway_tag }}
CORE_TAG: ${{ github.event.inputs.core_tag || '2' }}
PROXY_TAG: ${{ github.event.inputs.proxy_tag || '2' }}
GATEWAY_TAG: ${{ github.event.inputs.gateway_tag || '2' }}
run: |
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
FILENAME="defguard_${TIMESTAMP}_core-${CORE_TAG}_edge-${PROXY_TAG}_gateway-${GATEWAY_TAG}.ova"
ls -lh output/defguard/defguard.ova
aws s3 cp output/defguard/defguard.ova "s3://defguard-downloads/ova/${FILENAME}"
echo "Uploaded: s3://defguard-downloads/ova/${FILENAME}"

- name: Publish as latest
if: ${{ inputs.publish_latest }}
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: eu-central-1
run: |
aws s3 cp output/defguard/defguard.ova "s3://defguard-downloads/ova/defguard-latest.ova" \
--cache-control "no-cache"
echo "Updated: s3://defguard-downloads/ova/defguard-latest.ova"
58 changes: 58 additions & 0 deletions .github/workflows/test-ova.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
name: Test OVA scripts

on:
push:
branches:
- main
paths:
- "ova/**"
- ".github/workflows/test-ova.yml"
pull_request:
branches:
- main
paths:
- "ova/**"
- ".github/workflows/test-ova.yml"

jobs:
logic:
name: Logic + compose config
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7

- name: Install bats
run: |
sudo apt-get update -qq
sudo apt-get install -y --no-install-recommends bats

- name: Run logic and config tests
run: bats ova/tests/start.bats ova/tests/generate-env.bats ova/tests/config.bats ova/tests/firewall.bats

integration:
name: Real bring-up
runs-on: [self-hosted, Linux, X64]
steps:
- name: Checkout
uses: actions/checkout@v7

- name: Install bats
run: |
sudo apt-get update -qq
sudo apt-get install -y --no-install-recommends bats

- name: Login to GitHub container registry
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Run integration tests
env:
RUN_INTEGRATION: "1"
CORE_TAG: "2"
PROXY_TAG: "2"
GATEWAY_TAG: "2"
run: bats ova/tests/integration.bats
1 change: 1 addition & 0 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
kubectl
kubernetes-helm
kubeconform
bats
];
};
});
Expand Down
15 changes: 15 additions & 0 deletions ova/defguard.pkr.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,16 @@ build {
destination = "/tmp/defguard-init.service"
}

provisioner "file" {
source = "files/defguard-firewall.sh"
destination = "/tmp/defguard-firewall.sh"
}

provisioner "file" {
source = "files/defguard-firewall.service"
destination = "/tmp/defguard-firewall.service"
}

provisioner "shell" {
inline = [
"sudo bash /tmp/docker-setup.sh",
Expand All @@ -107,8 +117,13 @@ build {
"echo 'DEFGUARD_GATEWAY_TAG=${var.gateway_tag}' | sudo tee -a /opt/stacks/defguard/.image-tags > /dev/null",
"sudo mv /tmp/99-defguard.cfg /etc/cloud/cloud.cfg.d/99-defguard.cfg",
"sudo mv /tmp/defguard-init.service /etc/systemd/system/defguard-init.service",
"sudo mv /tmp/defguard-firewall.sh /opt/stacks/defguard/defguard-firewall.sh",
"sudo chmod +x /opt/stacks/defguard/defguard-firewall.sh",
"sudo mv /tmp/defguard-firewall.service /etc/systemd/system/defguard-firewall.service",
"sudo systemctl daemon-reload",
"sudo systemctl enable docker.service",
"sudo systemctl enable defguard-init.service",
"sudo systemctl enable defguard-firewall.service",
"sudo chown -R ubuntu:ubuntu /opt/stacks/defguard",
"sudo rm -f /etc/netplan/00-installer-config.yaml /etc/netplan/50-cloud-init.yaml",
"sudo cloud-init clean --logs",
Expand Down
13 changes: 13 additions & 0 deletions ova/files/defguard-firewall.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[Unit]
Description=DefGuard host forwarding rules for the WireGuard gateway
After=docker.service
Wants=docker.service

[Service]
Type=oneshot
StandardOutput=append:/var/log/defguard-startup.log
StandardError=append:/var/log/defguard-startup.log
ExecStart=/bin/bash /opt/stacks/defguard/defguard-firewall.sh

[Install]
WantedBy=multi-user.target
37 changes: 37 additions & 0 deletions ova/files/defguard-firewall.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#!/bin/bash
# Docker sets the FORWARD policy to DROP and only accepts its own bridges, which
# drops the gateway's decrypted WireGuard traffic before it can be forwarded.

set -u

SYSCTL_DIR="${DEFGUARD_SYSCTL_DIR:-/etc/sysctl.d}"

# Docker enables net.ipv4.ip_forward itself, but not IPv6 forwarding.
cat > "$SYSCTL_DIR/99-defguard-forward.conf" <<EOF
net.ipv4.ip_forward = 1
net.ipv6.conf.all.forwarding = 1
EOF
sysctl --system >/dev/null

# DOCKER-USER only exists once dockerd has set up its chains; this may race
# docker.service. Wait briefly, then bail cleanly (next boot re-applies).
for _ in $(seq 1 30); do
if iptables -n -L DOCKER-USER >/dev/null 2>&1; then
break
fi
sleep 1
done

if ! iptables -n -L DOCKER-USER >/dev/null 2>&1; then
echo "DefGuard: DOCKER-USER chain not present; skipping (Docker not ready)."
exit 0
fi

for ipt in iptables ip6tables; do
for dir in "-i" "-o"; do
"$ipt" -C DOCKER-USER "$dir" wg+ -j ACCEPT 2>/dev/null \
|| "$ipt" -I DOCKER-USER "$dir" wg+ -j ACCEPT
done
done

echo "DefGuard: wg+ forwarding whitelisted in DOCKER-USER."
3 changes: 3 additions & 0 deletions ova/files/defguard-init.service
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,6 @@ StandardOutput=append:/var/log/defguard-startup.log
StandardError=append:/var/log/defguard-startup.log
ExecStart=/bin/bash /opt/stacks/defguard/generate-env.sh
ExecStart=/bin/bash /opt/stacks/defguard/start.sh

[Install]
WantedBy=multi-user.target
7 changes: 4 additions & 3 deletions ova/files/generate-env.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
# Generates /opt/stacks/defguard/.env with random secrets on first boot.
# If .env already exists (e.g. provided via cloud-init), this script does nothing.

ENV_FILE="/opt/stacks/defguard/.env"
STACK_DIR="${DEFGUARD_STACK_DIR:-/opt/stacks/defguard}"
ENV_FILE="$STACK_DIR/.env"

if [ -f "$ENV_FILE" ]; then
echo "DefGuard: .env already exists, skipping generation."
Expand All @@ -13,8 +14,8 @@ echo "DefGuard: generating .env with random secrets..."

DB_PASSWORD=$(openssl rand -hex 16)

if [ -f "/opt/stacks/defguard/.image-tags" ]; then
source "/opt/stacks/defguard/.image-tags"
if [ -f "$STACK_DIR/.image-tags" ]; then
source "$STACK_DIR/.image-tags"
fi

: "${DEFGUARD_CORE_TAG:?DEFGUARD_CORE_TAG is required}"
Expand Down
11 changes: 6 additions & 5 deletions ova/files/start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@
# - path: /opt/stacks/defguard/enable-docker-management
# content: ""

PROFILES_FILE="/opt/stacks/defguard/active-profiles"
ENABLE_DOCKER_MGMT_FILE="/opt/stacks/defguard/enable-docker-management"
STACK_DIR="${DEFGUARD_STACK_DIR:-/opt/stacks/defguard}"
PROFILES_FILE="$STACK_DIR/active-profiles"
ENABLE_DOCKER_MGMT_FILE="$STACK_DIR/enable-docker-management"

# Append the dockge profile if the opt-in flag file is present
_maybe_add_dockge() {
Expand All @@ -33,7 +34,7 @@ if [ ! -f "$PROFILES_FILE" ]; then
if [ -n "$COMPOSE_PROFILES" ]; then
export COMPOSE_PROFILES
fi
docker compose -f /opt/stacks/defguard/docker-compose.yaml up -d
docker compose -f "$STACK_DIR/docker-compose.yaml" up -d
else
COMPOSE_PROFILES=$(tr '[:space:]' ',' < "$PROFILES_FILE" | tr -s ',' | sed 's/,$//')
if [ -z "$COMPOSE_PROFILES" ]; then
Expand All @@ -44,10 +45,10 @@ else
else
unset COMPOSE_PROFILES
fi
docker compose -f /opt/stacks/defguard/docker-compose.yaml up -d
docker compose -f "$STACK_DIR/docker-compose.yaml" up -d
else
COMPOSE_PROFILES=$(_maybe_add_dockge "$COMPOSE_PROFILES")
export COMPOSE_PROFILES
docker compose -f /opt/stacks/defguard/docker-compose.standalone.yaml up -d
docker compose -f "$STACK_DIR/docker-compose.standalone.yaml" up -d
fi
fi
49 changes: 49 additions & 0 deletions ova/tests/config.bats
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#!/usr/bin/env bats
# `docker compose config` evaluates profiles without pulling images, so the
# expected service set per profile combination can be checked offline.

load helpers

setup() {
command -v docker >/dev/null 2>&1 || skip "docker not installed"
docker compose version >/dev/null 2>&1 || skip "docker compose v2 not available"
make_stack
write_env
}

teardown() {
teardown_stack
}

# sorted, space-joined active services for a given file ($1) and profiles ($2)
services_for() {
COMPOSE_PROFILES="$2" docker compose -f "$STACK_DIR/$1" config --services 2>/dev/null | sort | xargs
}

@test "all-in-one without profiles -> core db edge gateway" {
[ "$(services_for docker-compose.yaml "")" = "core db edge gateway" ]
}

@test "all-in-one with dockge -> core db dockge edge gateway" {
[ "$(services_for docker-compose.yaml "dockge")" = "core db dockge edge gateway" ]
}

@test "standalone core -> core db" {
[ "$(services_for docker-compose.standalone.yaml "core")" = "core db" ]
}

@test "standalone core,gateway -> core db gateway" {
[ "$(services_for docker-compose.standalone.yaml "core,gateway")" = "core db gateway" ]
}

@test "standalone core,edge,gateway -> core db edge gateway" {
[ "$(services_for docker-compose.standalone.yaml "core,edge,gateway")" = "core db edge gateway" ]
}

@test "standalone core,edge,gateway,dockge -> core db dockge edge gateway" {
[ "$(services_for docker-compose.standalone.yaml "core,edge,gateway,dockge")" = "core db dockge edge gateway" ]
}

@test "standalone with no profiles -> no services" {
[ "$(services_for docker-compose.standalone.yaml "")" = "" ]
}
58 changes: 58 additions & 0 deletions ova/tests/firewall.bats
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#!/usr/bin/env bats
# iptables/ip6tables/sysctl/sleep are stubbed so only defguard-firewall.sh's
# rule logic is exercised, with no effect on the host firewall.

load helpers

setup() {
BIN="$(mktemp -d)"
cp "$STUB_DIR/iptables-stub" "$BIN/iptables"
cp "$STUB_DIR/iptables-stub" "$BIN/ip6tables"
cp "$STUB_DIR/noop" "$BIN/sysctl"
cp "$STUB_DIR/noop" "$BIN/sleep"
chmod +x "$BIN"/*
export PATH="$BIN:$PATH"
export IPT_STUB_LOG="$BIN/ipt.log"
: > "$IPT_STUB_LOG"
export DEFGUARD_SYSCTL_DIR="$BIN/sysctl.d"
mkdir "$DEFGUARD_SYSCTL_DIR"
}

teardown() {
rm -rf "$BIN"
}

inserts() {
grep -c -- '-I DOCKER-USER' "$IPT_STUB_LOG"
}

@test "whitelists wg+ in/out for both iptables and ip6tables" {
run bash "$FILES_DIR/defguard-firewall.sh"
[ "$status" -eq 0 ]
[ "$(inserts)" -eq 4 ]
grep -q -- 'iptables -I DOCKER-USER -i wg+ -j ACCEPT' "$IPT_STUB_LOG"
grep -q -- 'iptables -I DOCKER-USER -o wg+ -j ACCEPT' "$IPT_STUB_LOG"
grep -q -- 'ip6tables -I DOCKER-USER -i wg+ -j ACCEPT' "$IPT_STUB_LOG"
grep -q -- 'ip6tables -I DOCKER-USER -o wg+ -j ACCEPT' "$IPT_STUB_LOG"
}

@test "writes the forwarding sysctl drop-in" {
run bash "$FILES_DIR/defguard-firewall.sh"
[ "$status" -eq 0 ]
conf="$DEFGUARD_SYSCTL_DIR/99-defguard-forward.conf"
[ -f "$conf" ]
grep -qx 'net.ipv4.ip_forward = 1' "$conf"
grep -qx 'net.ipv6.conf.all.forwarding = 1' "$conf"
}

@test "idempotent: no inserts when the rules already exist" {
IPT_RULE_EXISTS=1 run bash "$FILES_DIR/defguard-firewall.sh"
[ "$status" -eq 0 ]
[ "$(inserts)" -eq 0 ]
}

@test "exits cleanly without inserts when DOCKER-USER is absent" {
IPT_CHAIN_EXISTS=0 run bash "$FILES_DIR/defguard-firewall.sh"
[ "$status" -eq 0 ]
[ "$(inserts)" -eq 0 ]
}
Loading