From d91f6ccdbcb5a533f16e68e90b8ab87941f125db Mon Sep 17 00:00:00 2001 From: Paulo Date: Mon, 7 Sep 2026 12:41:39 +0200 Subject: [PATCH] Publish the secrets proxy as an image The proxy ran from the official mitmproxy image with the addon mounted from a checkout, so a deployment outside this repository had to fetch the addon file first. deploy/proxy now has a Dockerfile that copies the addon into the pinned mitmproxy image. The main-merge workflow publishes it as ghcr.io/czpython/drukbox/proxy with the tags of the API image, and the pull-request workflow builds it. The addon takes the exchange address from SECRETS_EXCHANGE_URL, so a deployment sets one variable instead of the whole command. The deploy doc's example uses the image. --- .github/workflows/on-main-merge.yml | 22 ++++++++++++++++++++++ .github/workflows/on-pull-request.yml | 3 +++ AGENTS.md | 2 +- deploy/proxy/Dockerfile | 5 +++++ deploy/proxy/swap.py | 8 +++++++- docs/architecture.md | 4 ++-- docs/deploy.md | 23 +++++++++++------------ 7 files changed, 51 insertions(+), 16 deletions(-) create mode 100644 deploy/proxy/Dockerfile diff --git a/.github/workflows/on-main-merge.yml b/.github/workflows/on-main-merge.yml index 792bf69..a3e0221 100644 --- a/.github/workflows/on-main-merge.yml +++ b/.github/workflows/on-main-merge.yml @@ -115,3 +115,25 @@ jobs: labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max + + - name: Extract proxy image metadata + id: proxy-meta + uses: docker/metadata-action@v6 + with: + images: ghcr.io/${{ github.repository }}/proxy + tags: | + type=ref,event=branch + type=sha + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build and push proxy image + uses: docker/build-push-action@v7 + with: + context: ./deploy/proxy + file: ./deploy/proxy/Dockerfile + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.proxy-meta.outputs.tags }} + labels: ${{ steps.proxy-meta.outputs.labels }} + cache-from: type=gha,scope=proxy + cache-to: type=gha,mode=max,scope=proxy diff --git a/.github/workflows/on-pull-request.yml b/.github/workflows/on-pull-request.yml index 02017ec..51172f0 100644 --- a/.github/workflows/on-pull-request.yml +++ b/.github/workflows/on-pull-request.yml @@ -133,6 +133,9 @@ jobs: - name: Build container image run: docker build -t drukbox:validate . + - name: Build proxy image + run: docker build -t drukbox-proxy:validate deploy/proxy + api-tests: name: Run API Tests (docker provider) runs-on: ubuntu-latest diff --git a/AGENTS.md b/AGENTS.md index 85137bb..9f30829 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -58,7 +58,7 @@ src/ templates/ # Template API, models, service, and janitor conftest.py # Test env defaults and database reset fixture alembic/ # Database migrations -deploy/proxy/ # The secrets proxy: a mitmproxy addon, mounted into the official image +deploy/proxy/ # The secrets proxy: a mitmproxy addon and the Dockerfile of its image api-tests/ # Playwright black-box API tests docs/ # Architecture, networking, deploy, add-a-provider Dockerfile # Single image: API + cron commands + migrations diff --git a/deploy/proxy/Dockerfile b/deploy/proxy/Dockerfile new file mode 100644 index 0000000..d4af3f2 --- /dev/null +++ b/deploy/proxy/Dockerfile @@ -0,0 +1,5 @@ +FROM mitmproxy/mitmproxy:12.2.3 + +COPY swap.py /addon/swap.py + +CMD ["mitmdump", "--listen-host=0.0.0.0", "--listen-port=8880", "--set=flow_detail=1", "-s", "/addon/swap.py"] diff --git a/deploy/proxy/swap.py b/deploy/proxy/swap.py index dfd8059..c6fe87a 100644 --- a/deploy/proxy/swap.py +++ b/deploy/proxy/swap.py @@ -12,6 +12,7 @@ import ipaddress import json import logging +import os import socket import time import urllib.parse @@ -105,7 +106,12 @@ def __init__(self) -> None: self._upstreams_known = False def load(self, loader) -> None: - loader.add_option("exchange_url", str, "", "Address of the secrets exchange process.") + loader.add_option( + "exchange_url", + str, + os.environ.get("SECRETS_EXCHANGE_URL", ""), + "Address of the secrets exchange process.", + ) def configure(self, updated: set[str]) -> None: if "exchange_url" in updated: diff --git a/docs/architecture.md b/docs/architecture.md index 98ae7f9..04e2f0e 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -32,7 +32,7 @@ hosts.service host lifecycle behavior (HostService) host_secrets.api host secret registration concerns only host_secrets built-in catalog, placeholders, delivery at provisioning secrets_exchange the secrets exchange process behind the secrets proxy -deploy/proxy the secrets proxy addon, run by the official mitmproxy image +deploy/proxy the secrets proxy addon and the Dockerfile of its image templates.api template request/response concerns only templates.service template build and delete behavior (TemplateService) providers/ one package per VM provider @@ -139,7 +139,7 @@ and the proxy's CA in `SECRETS_PROXY_CA`, which it installs at boot. A box with a `github` secret also points git at gh for its credential and rewrites SSH remotes to HTTPS, so git sends the placeholder as a Basic password and the proxy swaps it. -The proxy is the official mitmproxy image with the addon in `deploy/proxy`. +The proxy is the official mitmproxy image with the addon in `deploy/proxy` built in. It terminates TLS only for the hosts the exchange lists at `/upstreams`, the hosts with a registered secret, and tunnels every other host blind. For a request with a placeholder it asks the exchange at `/authorize`, with the diff --git a/docs/deploy.md b/docs/deploy.md index 67c8d32..9c9736d 100644 --- a/docs/deploy.md +++ b/docs/deploy.md @@ -8,7 +8,9 @@ behind these defaults, read [Security](security.md). One image serves everything — API, maintenance commands, migrations. It's published to `ghcr.io/czpython/drukbox` on every release; build -`docker build -t ghcr.io/czpython/drukbox .` only to run a local change. +`docker build -t ghcr.io/czpython/drukbox .` only to run a local change. The +secrets proxy image, `ghcr.io/czpython/drukbox/proxy`, is published beside it +with the same tags. ```bash IMAGE=ghcr.io/czpython/drukbox:latest @@ -274,8 +276,11 @@ A sandbox never holds a real third-party credential. It holds a placeholder, and it sends its HTTPS through the secrets proxy. The proxy swaps the placeholder for the real credential on the way out. Two pieces run this: -- **The proxy** is the official `mitmproxy/mitmproxy` image with the addon - `deploy/proxy/swap.py` mounted in. It terminates TLS only for the hosts +- **The proxy** is `ghcr.io/czpython/drukbox/proxy`: the official + `mitmproxy/mitmproxy` image with the addon `deploy/proxy/swap.py` built in. + It listens on 8880 and reads the exchange address from + `SECRETS_EXCHANGE_URL`. A checkout can mount the addon into the official + image instead. It terminates TLS only for the hosts that have a registered secret and tunnels every other host blind. It refuses a destination that resolves to a loopback, private, link-local, or metadata address. It makes its CA on first start and keeps it in a volume. @@ -294,18 +299,12 @@ services: SECRETS_EXCHANGE_BIND_HOST: 0.0.0.0 proxy: - image: mitmproxy/mitmproxy:12.2.3 - command: - - mitmdump - - --listen-host=0.0.0.0 - - --listen-port=8880 - - --set=exchange_url=http://exchange:8781 - - --set=flow_detail=1 - - -s=/addon/swap.py + image: ghcr.io/czpython/drukbox/proxy:latest + environment: + SECRETS_EXCHANGE_URL: http://exchange:8781 ports: - "8880:8880" volumes: - - ./deploy/proxy/swap.py:/addon/swap.py:ro - secrets-proxy-ca:/home/mitmproxy/.mitmproxy volumes: