4 Commits

Author SHA1 Message Date
torben 4548aa161f Add FluxCD Copilot prompt for deployment instructions in README
Build and Test / verify (push) Successful in 31s
2026-07-12 18:01:21 +02:00
torben 9ab31a9827 Fix Dockerfile by re-adding GOPROXY_URL argument for clarity
Build and Test / verify (push) Successful in 32s
Build and Push Container Image / build-and-push-image (push) Successful in 50s
2026-07-12 17:52:57 +02:00
torben b8d18e819c Add logging functions and improve error handling in test registry script
Build and Test / verify (push) Successful in 32s
Build and Push Container Image / build-and-push-image (push) Successful in 52s
2026-07-12 15:55:20 +02:00
torben 8ca0fa8611 Enhance release workflow and add local testing script for registry images
Build and Test / verify (push) Successful in 33s
- Update release.yaml to publish :latest and :prerelease-tag for stable and pre-release tags respectively.
- Add README section for local testing of registry images with a new script.
- Introduce scripts/test-registry-image.sh for end-to-end testing of published images.
2026-07-12 15:48:31 +02:00
5 changed files with 375 additions and 5 deletions
+7 -5
View File
@@ -48,18 +48,20 @@ jobs:
# Note: This repository only builds and pushes images. # Note: This repository only builds and pushes images.
# FluxCD Image Automation in another repository performs deployment. # FluxCD Image Automation in another repository performs deployment.
# Stable tags (vX.Y.Z) may become :latest. Pre-release tags never do. # Stable tags (vX.Y.Z) also publish :latest.
if [[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then # Pre-release tags (e.g. vX.Y.Z-rc1) also publish :prerelease-tag.
if printf '%s' "$TAG" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+$'; then
docker buildx build \ docker buildx build \
--platform linux/amd64 \ --platform linux/amd64 \
--push \ --push \
--tag $REGISTRY/$IMAGE_NAME:$TAG \ --tag "$REGISTRY/$IMAGE_NAME:$TAG" \
--tag $REGISTRY/$IMAGE_NAME:latest \ --tag "$REGISTRY/$IMAGE_NAME:latest" \
. .
else else
docker buildx build \ docker buildx build \
--platform linux/amd64 \ --platform linux/amd64 \
--push \ --push \
--tag $REGISTRY/$IMAGE_NAME:$TAG \ --tag "$REGISTRY/$IMAGE_NAME:$TAG" \
--tag "$REGISTRY/$IMAGE_NAME:prerelease-tag" \
. .
fi fi
+1
View File
@@ -3,6 +3,7 @@ ARG GOPROXY_URL=https://gitea.nehmer.net/api/packages/torben/go,https://proxy.go
FROM golang:${GO_VERSION}-bookworm AS build FROM golang:${GO_VERSION}-bookworm AS build
WORKDIR /src WORKDIR /src
ARG GOPROXY_URL
ENV GOPROXY=${GOPROXY_URL} ENV GOPROXY=${GOPROXY_URL}
COPY go.mod ./ COPY go.mod ./
+89
View File
@@ -157,6 +157,54 @@ docker run --rm -p 8080:8080 \
gitea-mcp-auth-proxy:dev gitea-mcp-auth-proxy:dev
``` ```
## Registry-Image lokal testen
Fuer einen End-to-End-Test gegen die veroeffentlichte Gitea-Container-Registry gibt es das Script `scripts/test-registry-image.sh`.
Es orientiert sich am Gitea-Flow aus der Container-Registry-Doku:
- Login gegen `gitea.nehmer.net`
- Pull von `gitea.nehmer.net/torben/gitea-mcp-auth-proxy:<tag>`
- lokaler Start des gezogenen Images mit den eingecheckten Demo-Hashes
- HTTP-Pruefungen fuer `200` und `401`
Default-Verhalten:
- User: `torben`
- Tag: `latest`
- Runtime: automatisch `docker`, sonst `podman`
Der Registry-Login fragt das Passwort oder einen PAT interaktiv und unsichtbar ab.
Auth-Handling ist absichtlich ephemeral:
- Das Script schreibt keine Credentials in `~/.docker/config.json`.
- Fuer `docker` wird ein temporäres `DOCKER_CONFIG`-Verzeichnis verwendet.
- Fuer `podman` wird eine temporäre `REGISTRY_AUTH_FILE` verwendet.
- Beides wird beim Script-Ende automatisch geloescht.
Beispiel:
```bash
./scripts/test-registry-image.sh
```
Bestimmten Tag testen:
```bash
./scripts/test-registry-image.sh --tag v1.2.3
```
Optional auf vorhandene Image-Labels pruefen:
```bash
./scripts/test-registry-image.sh \
--require-label org.opencontainers.image.source \
--require-label org.opencontainers.image.title=gitea-mcp-auth-proxy
```
Hinweis: Das Script validiert optionale Labels erst nach dem Pull. Aktuell definiert dieses Repo selbst noch keine OCI-Image-Labels im Build.
## CI/CD (Gitea Actions) ## CI/CD (Gitea Actions)
- Build-Workflow: `.gitea/workflows/build.yaml` - Build-Workflow: `.gitea/workflows/build.yaml`
@@ -181,6 +229,47 @@ Referenzbeispiele fuer lokale Verifikation liegen unter:
Diese Manifeste sind bewusst minimal und nicht als produktive FluxCD-Quelle gedacht. Diese Manifeste sind bewusst minimal und nicht als produktive FluxCD-Quelle gedacht.
## FluxCD Copilot Prompt
The following prompt can be used to instruct GitHub Copilot in your FluxCD repository about this service:
---
> **Prompt for FluxCD repo Copilot:**
>
> I am deploying `gitea-mcp-auth-proxy`, a Traefik ForwardAuth service that validates `Authorization: Bearer <token>` headers for gitea-mcp access control.
>
> **Image:** `gitea.nehmer.net/torben/gitea-mcp-auth-proxy:<tag>`
> Published to the Gitea container registry at `gitea.nehmer.net`. Stable releases use `vX.Y.Z` tags; `latest` is only updated for stable releases, never for pre-releases.
>
> **How it works:**
> - Returns `200` for a valid bearer token, `401` otherwise.
> - `GET /healthz` is always reachable without auth (use for readiness/liveness probes).
> - Token validation uses SHA-256 hashes only — never plaintext tokens.
>
> **Required configuration (env):**
> - `AUTH_PROXY_LISTEN_ADDR` — default `:8080`
> - `AUTH_PROXY_TOKEN_HASHES_DIR` — path to a directory where each file contains one SHA-256 token hash; ideal for a Kubernetes Secret volume mount (e.g. `/var/run/secrets/auth-proxy`)
> - `AUTH_PROXY_TOKEN_HASHES` — alternative: comma-separated list of SHA-256 hashes (64 hex chars each, optionally prefixed with `sha256:`)
> - At least one of the two hash sources must be configured; the service exits on startup if no tokens are loaded.
> - `AUTH_PROXY_LOG_LEVEL` — optional, default `info`
>
> **Secret:** Create a Kubernetes Secret named `gitea-mcp-auth-proxy-tokens` containing one file per allowed token, where each file contains the SHA-256 hash of the token (not the token itself). Mount it at `/var/run/secrets/auth-proxy` as a read-only volume.
>
> Generate a hash with: `echo -n "my-token" | sha256sum | awk '{print $1}'`
>
> **Deployment constraints:**
> - Service must be `ClusterIP` only — do not expose externally.
> - Do not create a public Ingress route for `/healthz`.
> - The service is consumed by Traefik as a ForwardAuth middleware; only Traefik and internal probes need network access.
> - Apply a NetworkPolicy that allows ingress only from Traefik (by namespace/pod label) and the kubelet (for probes).
> - Image pull requires credentials for `gitea.nehmer.net`; configure an `imagePullSecret` referencing a Secret with registry credentials.
>
> **Reference manifests** (minimal, non-authoritative) are available at:
> `https://gitea.nehmer.net/torben/gitea-mcp-forward-auth/src/branch/main/deploy/k3s/`
---
## Annahmen ## Annahmen
- Annahme: Go-Version ist `1.26`. - Annahme: Go-Version ist `1.26`.
View File
+278
View File
@@ -0,0 +1,278 @@
#!/usr/bin/env bash
set -euo pipefail
REGISTRY_HOST="gitea.nehmer.net"
OWNER="torben"
IMAGE_NAME="gitea-mcp-auth-proxy"
TAG="latest"
USERNAME="torben"
RUNTIME=""
REQUIRED_LABELS=()
log_ts() {
date '+%Y-%m-%d %H:%M:%S'
}
log_info() {
printf '[%s] [INFO] %s\n' "$(log_ts)" "$*"
}
log_ok() {
printf '[%s] [ OK ] %s\n' "$(log_ts)" "$*"
}
log_warn() {
printf '[%s] [WARN] %s\n' "$(log_ts)" "$*" >&2
}
log_fail() {
printf '[%s] [FAIL] %s\n' "$(log_ts)" "$*" >&2
}
usage() {
cat <<'EOF'
Usage: scripts/test-registry-image.sh [options]
Pulls the published container image from the Gitea registry, starts it locally
with the committed demo hashes, and verifies expected HTTP responses.
Options:
--tag <tag> Image tag to test (default: latest)
--runtime <docker|podman> Container runtime to use (auto-detect by default)
--username <user> Registry username (default: torben)
--require-label <key> Require image label to exist
--require-label <k=v> Require image label to equal a specific value
-h, --help Show this help
Examples:
scripts/test-registry-image.sh
scripts/test-registry-image.sh --tag v1.2.3
scripts/test-registry-image.sh --require-label org.opencontainers.image.source
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
--tag)
TAG="${2:-}"
shift 2
;;
--runtime)
RUNTIME="${2:-}"
shift 2
;;
--username)
USERNAME="${2:-}"
shift 2
;;
--require-label)
REQUIRED_LABELS+=("${2:-}")
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown argument: $1" >&2
usage >&2
exit 1
;;
esac
done
if [[ -z "$TAG" ]]; then
log_fail "Tag must not be empty"
exit 1
fi
if [[ -z "$RUNTIME" ]]; then
if command -v docker >/dev/null 2>&1; then
RUNTIME="docker"
log_info "Runtime auto-detected: docker"
elif command -v podman >/dev/null 2>&1; then
RUNTIME="podman"
log_info "Runtime auto-detected: podman"
else
log_fail "Neither docker nor podman is available"
exit 1
fi
fi
if ! command -v "$RUNTIME" >/dev/null 2>&1; then
log_fail "Container runtime not found: $RUNTIME"
exit 1
fi
if ! command -v curl >/dev/null 2>&1; then
log_fail "curl is required"
exit 1
fi
log_info "Using runtime: $RUNTIME"
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
repo_root="$(cd "$script_dir/.." && pwd)"
hash_dir="$repo_root/demo/token-hashes"
if [[ ! -d "$hash_dir" ]]; then
log_fail "Demo hash directory not found: $hash_dir"
exit 1
fi
image_ref="$REGISTRY_HOST/$OWNER/$IMAGE_NAME:$TAG"
container_name="authproxy-registry-test-$$"
container_id=""
host_port=""
tmp_dir=""
auth_mode=""
cleanup() {
local exit_code=$?
if [[ -n "$container_id" ]]; then
log_info "Cleaning up container: $container_id"
"$RUNTIME" rm -f "$container_id" >/dev/null 2>&1 || true
fi
if [[ -n "$tmp_dir" && -d "$tmp_dir" ]]; then
log_info "Removing temporary auth directory"
rm -rf "$tmp_dir"
fi
exit "$exit_code"
}
trap cleanup EXIT
tmp_dir="$(mktemp -d)"
log_info "Created temporary workspace: $tmp_dir"
if [[ "$RUNTIME" == "docker" ]]; then
auth_mode="docker_config"
mkdir -p "$tmp_dir/docker-config"
log_info "Auth mode: docker with temporary DOCKER_CONFIG"
elif [[ "$RUNTIME" == "podman" ]]; then
auth_mode="podman_auth_file"
log_info "Auth mode: podman with temporary REGISTRY_AUTH_FILE"
else
log_fail "Unsupported runtime: $RUNTIME"
exit 1
fi
log_info "Test image: $image_ref"
log_info "Demo hash source: $hash_dir"
printf 'Registry password for %s@%s: ' "$USERNAME" "$REGISTRY_HOST" >&2
stty -echo
IFS= read -r registry_password
stty echo
printf '\n' >&2
if [[ -z "$registry_password" ]]; then
log_fail "Empty password/token is not allowed"
exit 1
fi
log_info "Logging in to registry (credentials are not printed)"
if [[ "$auth_mode" == "docker_config" ]]; then
printf '%s' "$registry_password" | \
env DOCKER_CONFIG="$tmp_dir/docker-config" \
"$RUNTIME" login "$REGISTRY_HOST" --username "$USERNAME" --password-stdin >/dev/null
elif [[ "$auth_mode" == "podman_auth_file" ]]; then
printf '%s' "$registry_password" | \
env REGISTRY_AUTH_FILE="$tmp_dir/podman-auth.json" \
"$RUNTIME" login "$REGISTRY_HOST" --username "$USERNAME" --password-stdin >/dev/null
fi
unset registry_password
log_ok "Registry login successful"
log_info "Pulling image: $image_ref"
if [[ "$auth_mode" == "docker_config" ]]; then
env DOCKER_CONFIG="$tmp_dir/docker-config" "$RUNTIME" pull "$image_ref" >/dev/null
else
env REGISTRY_AUTH_FILE="$tmp_dir/podman-auth.json" "$RUNTIME" pull "$image_ref" >/dev/null
fi
log_ok "Image pull complete"
for required_label in "${REQUIRED_LABELS[@]}"; do
log_info "Validating required label: $required_label"
label_key="$required_label"
label_expected=""
if [[ "$required_label" == *=* ]]; then
label_key="${required_label%%=*}"
label_expected="${required_label#*=}"
fi
label_value="$("$RUNTIME" image inspect "$image_ref" --format "{{ index .Config.Labels \"$label_key\" }}" 2>/dev/null || true)"
if [[ -z "$label_value" || "$label_value" == "<no value>" ]]; then
log_fail "Required image label missing: $label_key"
exit 1
fi
if [[ -n "$label_expected" && "$label_value" != "$label_expected" ]]; then
log_fail "Image label mismatch for $label_key: expected $label_expected, got $label_value"
exit 1
fi
log_ok "Label check passed: $label_key=$label_value"
done
log_info "Starting container from image"
container_id="$($RUNTIME run -d \
--name "$container_name" \
-p 127.0.0.1::8080 \
-e AUTH_PROXY_LISTEN_ADDR=:8080 \
-e AUTH_PROXY_TOKEN_HASHES_DIR=/token-hashes \
-e AUTH_PROXY_LOG_LEVEL=debug \
-v "$hash_dir:/token-hashes:ro" \
"$image_ref")"
log_ok "Container started: $container_id"
host_port="$($RUNTIME port "$container_id" 8080/tcp | awk -F: 'NR==1 {print $NF}')"
if [[ -z "$host_port" ]]; then
log_fail "Failed to determine mapped host port"
"$RUNTIME" logs "$container_id" >&2 || true
exit 1
fi
base_url="http://127.0.0.1:$host_port"
log_info "Container is reachable at: $base_url"
wait_for_health() {
local attempt http_code
log_info "Waiting for health endpoint to return 200"
for attempt in $(seq 1 30); do
http_code="$(curl -s -o /dev/null -w '%{http_code}' "$base_url/healthz" || true)"
if [[ "$http_code" == "200" ]]; then
log_ok "Health endpoint is ready after $attempt attempt(s)"
return 0
fi
log_info "Health attempt $attempt/30 returned: $http_code"
sleep 1
done
return 1
}
assert_status() {
local expected=$1
local description=$2
shift 2
local http_code
http_code="$(curl -s -o /dev/null -w '%{http_code}' "$@")"
if [[ "$http_code" != "$expected" ]]; then
log_fail "$description expected $expected, got $http_code"
"$RUNTIME" logs "$container_id" >&2 || true
exit 1
fi
log_ok "$description -> $http_code"
}
if ! wait_for_health; then
log_fail "Container did not become healthy: $image_ref"
"$RUNTIME" logs "$container_id" >&2 || true
exit 1
fi
log_info "Running HTTP assertions"
assert_status 200 "healthz" "$base_url/healthz"
assert_status 401 "missing token" "$base_url/"
assert_status 401 "wrong token" -H "Authorization: Bearer wrong" "$base_url/"
assert_status 200 "demo-token-1" -H "Authorization: Bearer demo-token-1" "$base_url/"
log_ok "Registry image test succeeded: $image_ref"