Skip to content

Commit a4a9cf1

Browse files
authored
Merge pull request #1 from dash14/migrate-haproxy
Replace nginx with HAProxy and introduce new rule syntax
2 parents 989a35c + 94aaccf commit a4a9cf1

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+1134
-427
lines changed

.github/workflows/example-restrict.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
uses: dash14/buildcage/setup@v1
1414
with:
1515
proxy_mode: restrict
16-
allowed_https_domains: registry.npmjs.org
16+
allowed_https_rules: registry.npmjs.org:443
1717

1818
- name: Set up Docker Buildx
1919
uses: docker/setup-buildx-action@v3

.github/workflows/test.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,19 @@ on:
1313
- main
1414

1515
jobs:
16+
unit_test:
17+
runs-on: ubuntu-latest
18+
permissions:
19+
contents: read
20+
name: test (unit)
21+
steps:
22+
- name: Checkout
23+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
24+
with:
25+
persist-credentials: false
26+
- name: Run unit tests
27+
run: make test_unit
28+
1629
test:
1730
runs-on: ubuntu-latest
1831
permissions:

Makefile

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,31 +5,32 @@ COMPOSE_FILE ?= compose.yml
55
help:
66
@grep -E '^[a-zA-Z_0-9-]+(-%)?:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
77

8+
.PHONY: clean
89
clean: ## Clean up all resources
910
@echo "Stopping and removing all containers..."
1011
@docker buildx rm buildcage 2>/dev/null || true
1112
@docker compose -f compose.yml -f compose.test.yml down -v --rmi all
1213
@docker rmi buildcage-test 2>/dev/null || true
1314

15+
.PHONY: run_audit_mode
1416
run_audit_mode: ## Start in audit mode
1517
@echo "Starting buildcage in AUDIT mode..."
1618
@COMPOSE_FILE=$(COMPOSE_FILE) \
1719
PROXY_MODE=audit \
18-
ALLOWED_HTTP_DOMAINS="" \
19-
ALLOWED_HTTPS_DOMAINS="" \
2020
docker compose up -d --wait --build
2121
@docker buildx rm buildcage 2>/dev/null || true
2222
@echo "Creating buildx builder..."
2323
@docker buildx create --bootstrap \
2424
--name buildcage \
2525
--driver remote tcp://localhost:1234
2626

27+
.PHONY: run_restrict_mode
2728
run_restrict_mode: ## Start in restrict mode
2829
@echo "Starting buildcage in RESTRICT mode..."
2930
@COMPOSE_FILE=$(COMPOSE_FILE) \
3031
PROXY_MODE=restrict \
31-
ALLOWED_HTTP_DOMAINS="$${ALLOWED_HTTP_DOMAINS:-}" \
32-
ALLOWED_HTTPS_DOMAINS="$${ALLOWED_HTTPS_DOMAINS:-github.com,registry.npmjs.org,api.github.com,objects.githubusercontent.com,httpbin.org,deb.debian.org,*.githubusercontent.com}" \
32+
ALLOWED_HTTP_RULES="$$(node setup/convert-rules.mjs "$${ALLOWED_HTTP_RULES:-}")" \
33+
ALLOWED_HTTPS_RULES="$$(node setup/convert-rules.mjs "$${ALLOWED_HTTPS_RULES:-github.com:443 registry.npmjs.org:443 api.github.com:443 objects.githubusercontent.com:443 httpbin.org:443 deb.debian.org:80 *.githubusercontent.com:443}")" \
3334
docker compose up -d --wait --build
3435
@docker buildx rm buildcage 2>/dev/null || true
3536
@echo "Creating buildx builder..."
@@ -64,3 +65,7 @@ test_audit_mode: ## Run audit mode tests
6465
@node report/main.mjs ./compose.yml
6566
@./test/assert-audit-mode.sh
6667
@$(MAKE) clean
68+
69+
.PHONY: test_unit
70+
test_unit: ## Run unit tests
71+
@node --test setup/lib/rules.test.mjs

README.md

Lines changed: 34 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ buildcage runs as a [remote driver](https://docs.docker.com/build/builders/drive
3434

3535
- HTTPS: SNI (Server Name Indication) for domain matching — TLS is not terminated
3636
- HTTP: Host header for domain matching
37-
- Direct IP access: blocked by iptables
37+
- Direct IP access: blocked unless explicitly allowed
3838

3939
**Two modes available** (see [Reference](#reference) for details):
4040

@@ -126,7 +126,7 @@ The report action outputs a Job Summary showing every domain your build contacte
126126
127127
<img src="assets/report-audit-mode.png" alt="Outbound Traffic Report - audit mode" width="556">
128128
129-
Copy these domain names into `allowed_https_domains` or `allowed_http_domains` for Step 3.
129+
Copy these domain names into `allowed_https_rules` or `allowed_http_rules` for Step 3.
130130

131131
#### Step 3: Create your allowlist and switch to restrict mode
132132

@@ -146,9 +146,9 @@ jobs:
146146
uses: dash14/buildcage/setup@v1
147147
with:
148148
proxy_mode: restrict # Block everything except allowed domains
149-
allowed_https_domains: >-
150-
registry.npmjs.org,
151-
fonts.googleapis.com
149+
allowed_https_rules: >-
150+
registry.npmjs.org:443
151+
fonts.googleapis.com:443
152152
153153
- name: Set up Docker Buildx
154154
uses: docker/setup-buildx-action@v3
@@ -184,7 +184,7 @@ Starts the buildcage builder container.
184184
uses: dash14/buildcage/setup@v1
185185
with:
186186
proxy_mode: restrict
187-
allowed_https_domains: registry.npmjs.org,github.com
187+
allowed_https_rules: registry.npmjs.org:443 github.com:443
188188
```
189189

190190
#### Parameters
@@ -194,22 +194,25 @@ Starts the buildcage builder container.
194194
| `buildcage_image` | No | `ghcr.io/<owner>/<repo>` | Docker image name |
195195
| `buildcage_version` | No | `1` | Image tag |
196196
| `proxy_mode` | No | `restrict` | Operation mode (`audit` / `restrict`) |
197-
| `allowed_http_domains` | No | empty | Allowed HTTP domains (comma-separated, without port) |
198-
| `allowed_https_domains` | No | empty | Allowed HTTPS domains (comma-separated, without port) |
199-
| `http_ports` | No | `80` | Comma-separated HTTP listen ports for the proxy |
200-
| `https_ports` | No | `443` | Comma-separated HTTPS listen ports for the proxy |
197+
| `allowed_https_rules` | No | empty | HTTPS allow rules (wildcard or regex, port required) |
198+
| `allowed_http_rules` | No | empty | HTTP allow rules (wildcard or regex, port required) |
199+
| `allowed_ip_rules` | No | empty | IP address allow rules (wildcard or regex, port required) |
201200
| `port` | No | `1234` | BuildKit endpoint port on localhost |
202201

203-
**Domain matching patterns**
204-
205-
The following patterns are supported for domain values:
202+
**Rule syntax**
206203

207204
| Pattern | Example | Matches |
208205
|---------|---------|---------|
209-
| Exact domain | `www.example.com` | Only `www.example.com` |
210-
| Prefix wildcard | `*.example.com` | `sub.example.com`, `deep.sub.example.com` (not `example.com` itself) |
211-
| Dot-prefix shorthand | `.example.com` | Both `example.com` and `*.example.com` |
212-
| Suffix wildcard | `example.*` | `example.com`, `example.io`, `example.org`, etc. |
206+
| Exact domain | `example.com:443` | `example.com` on port 443 only |
207+
| Single-level wildcard | `*.example.com:443` | `sub.example.com` on port 443 (not `deep.sub.example.com`) |
208+
| Multi-level wildcard | `**.example.com:443` | `sub.example.com` and `deep.sub.example.com` on port 443 |
209+
| Single-char wildcard | `exampl?.com:443` | `example.com`, `examplx.com` on port 443 |
210+
| Wildcard port | `example.com:*` | `example.com` on any port |
211+
| Regex | `~^custom\.pattern:\d+$` | Matched against `domain:port` |
212+
213+
IP address rules (e.g., `192.168.1.1:443`) use the same syntax but go in `allowed_ip_rules`.
214+
215+
For detailed syntax, see [Rule Syntax](./docs/rules.md).
213216

214217
#### Outputs
215218

@@ -243,7 +246,7 @@ Pass this port to [`docker/setup-buildx-action`](https://github.com/docker/setup
243246
**When to use:** Production builds, CI/CD pipelines, security-critical environments.
244247

245248
**What it does:**
246-
- Allows connections only to domains in `allowed_http_domains` / `allowed_https_domains`
249+
- Allows connections only to domains in `allowed_http_rules` / `allowed_https_rules`
247250
- Blocks all other connections
248251
- Logs allowed and blocked attempts
249252

@@ -252,18 +255,18 @@ Pass this port to [`docker/setup-buildx-action`](https://github.com/docker/setup
252255
- Start with audit mode to discover required domains, then switch to restrict mode.
253256
- Separate HTTP and HTTPS domains — some services use different hosts for each protocol.
254257
- Common package registries often use multiple domains (e.g., PyPI uses both `pypi.org` and `files.pythonhosted.org`).
255-
- Some package managers download over plain HTTP (e.g., certain Debian mirrors). Add those domains to `allowed_http_domains` separately:
258+
- Some package managers download over plain HTTP (e.g., certain Debian mirrors). Add those domains to `allowed_http_rules` separately:
256259

257260
```yaml
258-
allowed_http_domains: deb.debian.org
259-
allowed_https_domains: registry.npmjs.org
261+
allowed_http_rules: deb.debian.org:80
262+
allowed_https_rules: registry.npmjs.org:443
260263
```
261264

262-
- If your build needs to listen on non-standard ports (e.g., an application server on port 8080), add them with `http_ports` / `https_ports`:
265+
- If your build needs to access non-standard ports, specify the port in the rule:
263266

264267
```yaml
265-
http_ports: "80,8080"
266-
https_ports: "443,8443"
268+
allowed_http_rules: "example.com:8080"
269+
allowed_https_rules: "example.com:8443"
267270
```
268271

269272
### Report Action (`dash14/buildcage/report`)
@@ -288,13 +291,13 @@ Use the domain names shown in the report to create your allowlist for restrict m
288291

289292
<img src="assets/report-restrict-mode.png" alt="Outbound Traffic Report - restrict mode" width="556">
290293

291-
The report step fails if blocked connections are detected, causing the workflow to fail. You can disable this by setting `fail_on_blocked: false`.
294+
In restrict mode, the report step fails if blocked connections are detected, causing the workflow to fail. You can disable this by setting `fail_on_blocked: false`. In audit mode, blocked connections (e.g., protocol errors) are reported but never cause the step to fail.
292295

293296
#### Parameters
294297

295298
| Parameter | Required | Default | Description |
296299
|-----------|----------|---------|-------------|
297-
| `fail_on_blocked` | No | `true` | Fail the step if blocked connections are detected |
300+
| `fail_on_blocked` | No | `true` | Fail the step if blocked connections are detected (restrict mode only; ignored in audit mode) |
298301

299302
---
300303

@@ -368,7 +371,7 @@ The only known bypass is **domain fronting** — a technique where an attacker s
368371

369372
- **Does this work with private package registries?**
370373

371-
Yes. Just add your private registry's domain to `allowed_https_domains`.
374+
Yes. Just add your private registry's domain to `allowed_https_rules` (e.g., `registry.example.com:443`).
372375

373376
- **What happens if I forget to add a required domain?**
374377

@@ -380,7 +383,7 @@ The only known bypass is **domain fronting** — a technique where an attacker s
380383

381384
- **Can I allow access to an IP address (e.g., `http://192.168.1.1`)?**
382385

383-
No. Currently, only domain-based URLs are supported in the allowlist. Direct IP address access is blocked. If there is demand for this feature, it may be considered in a future release.
386+
Yes. Add the IP address with a port to `allowed_ip_rules` (e.g., `192.168.1.1:443`). Only IPv4 addresses are supported; CIDR notation is not supported.
384387

385388
- **Does this protect against malicious code execution?**
386389

@@ -424,6 +427,7 @@ See [LICENSE](./LICENSE) file for more details.
424427

425428
buildcage is built on top of:
426429
- [BuildKit](https://github.com/moby/buildkit) - Modern build toolkit
427-
- [nginx](https://nginx.org/) - HTTP proxy
428-
- [dnsmasq](https://thekelleys.org.uk/dnsmasq/doc.html) - DNS server
429430
- [CNI](https://github.com/containernetworking/cni) - Container network interface
431+
- [HAProxy](https://www.haproxy.org/) - TCP/HTTP proxy
432+
- [dnsmasq](https://thekelleys.org.uk/dnsmasq/doc.html) - DNS server
433+
- [s6-overlay](https://github.com/just-containers/s6-overlay) - Process supervisor

assets/banner.png

225 Bytes
Loading

assets/diagram-architecture.png

1.99 KB
Loading

assets/diagram-overview.png

339 Bytes
Loading

assets/report-restrict-mode.png

30 KB
Loading

compose.test.yml

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
services:
2-
# Point builder's resolver to test-dns + configure test domains
2+
# Point builder's resolver to test-dns + configure test rules
33
builder:
44
environment:
5-
- EXTERNAL_RESOLVER=10.200.0.53 valid=300s
6-
- ALLOWED_HTTP_DOMAINS=allowed.example.com,*.wildcard.example.com
7-
- ALLOWED_HTTPS_DOMAINS=allowed.example.com,*.wildcard.example.com
8-
- HTTP_PORTS=80,8080
9-
- HTTPS_PORTS=443,8443
5+
- EXTERNAL_RESOLVER=10.200.0.53
6+
- ALLOWED_HTTPS_RULES=^allowed\.example\.com:(443|8443)$$ ^[^.]+\.wildcard\.example\.com:(443|8443)$$
7+
- ALLOWED_HTTP_RULES=^allowed\.example\.com:(80|8080)$$ ^[^.]+\.wildcard\.example\.com:(80|8080)$$
108
networks:
119
default:
1210
test-net:

compose.yml

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,13 @@ services:
88
- "${PORT:-1234}:1234"
99
environment:
1010
- PROXY_MODE=${PROXY_MODE:-restrict}
11-
- ALLOWED_HTTP_DOMAINS=${ALLOWED_HTTP_DOMAINS:-}
12-
- ALLOWED_HTTPS_DOMAINS=${ALLOWED_HTTPS_DOMAINS:-}
13-
- HTTP_PORTS=${HTTP_PORTS:-80}
14-
- HTTPS_PORTS=${HTTPS_PORTS:-443}
15-
- EXTERNAL_RESOLVER=${EXTERNAL_RESOLVER:-8.8.8.8 8.8.4.4 valid=300s}
11+
- ALLOWED_HTTPS_RULES=${ALLOWED_HTTPS_RULES:-}
12+
- ALLOWED_HTTP_RULES=${ALLOWED_HTTP_RULES:-}
13+
- ALLOWED_IP_RULES=${ALLOWED_IP_RULES:-}
14+
- EXTERNAL_RESOLVER=${EXTERNAL_RESOLVER:-1.1.1.1,8.8.8.8}
1615
restart: unless-stopped
1716
healthcheck:
18-
test: ["CMD", "sh", "-c", "curl -sf --unix-socket /var/run/nginx-health.sock http://localhost/health && nc -z 127.0.0.1 1234"]
17+
test: ["CMD", "sh", "-c", "curl -sf --unix-socket /var/run/haproxy-health.sock http://localhost/health && nc -z 127.0.0.1 1234"]
1918
interval: 30s
2019
timeout: 5s
2120
retries: 10

0 commit comments

Comments
 (0)