# Gitea Anubis protection This repository deploys Anubis 1.27.0 in front of selected public Gitea paths on `code.brunner.ninja`. It uses a pinned container image, a Traefik `ForwardAuth` middleware, restricted Gitea Actions deployment credentials, and an out-of-repository signing key. ## LAN and CI bypass Traefik sends clients in `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, and `fc00::/7` directly to `legacy-gitea-service`, without Anubis middleware. This priority-500 route covers every path, including Git HTTP, action downloads, API requests and comparisons. The homelab Gitea runner uses this bypass too. The separate priority-400 route sends internet requests for `/.within.website` to Anubis. Do not combine the LAN match with that route: doing so sends LAN requests directly to Anubis instead of bypassing it. `ClientIP` matches the connection peer, not `X-Forwarded-For` or `X-Real-IP`. Traefik's LoadBalancer must retain `externalTrafficPolicy: Local` to preserve internet source addresses. The homelab's NAT loopback currently presents LAN requests as `192.168.0.254`; internet clients retain their public address. If a proxy or NAT configuration changes, repeat both checks below before assuming this trust boundary still holds. From outside the homelab, verify the redirect to Anubis and then direct LAN access (HTTP 200 with Gitea HTML, no Anubis challenge): ```sh curl -v https://code.brunner.ninja/papnkukn/qrcode-svg ssh dahoam curl -v https://code.brunner.ninja/papnkukn/qrcode-svg ssh dahoam git ls-remote https://code.brunner.ninja/actions/checkout.git refs/tags/v4 ``` ## Protected internet routes Anubis is used for: - repository and API paths belonging to the current third-party mirror owners; - selected oversized public `feedc0de` forks (at least 512 MiB); - comparison pages under every owner, including `feedc0de`. The existing catch-all Gitea route remains unprotected, so login, about, explore, owner pages, and ordinary `feedc0de/*` repositories remain unchanged outside their comparison pages. Proof-of-work difficulty is 5 and authorization cookies expire after one hour. Each additional difficulty level multiplies expected work by 16. Solve time is random and depends on the client's CPU and browser. ## Repository contents - `anubis.yaml`: policy ConfigMap, Deployment, Service, Middleware, and IngressRoute. - `install.sh`: signing-key creation, server-side dry-run, apply, and rollout. - `robots.txt`: the matching Gitea crawler policy installed on arschrock. - `test.sh`: local shell and Kubernetes schema validation. - `ci-deployer.yaml`: restricted Gitea Actions identity and revocable token. - `create-ci-kubeconfig.sh`: one-time CI credential bootstrap. - `.gitea/workflows/deploy.yml`: validation for pushes and pull requests, plus deployment after successful validation of `main`. ## Install manually With an administrator kubeconfig active, run: ```sh ./install.sh ``` The installer creates `default/gitea-anubis-key` only if it is missing. That Secret contains Anubis's Ed25519 signing key and is deliberately neither stored in Git nor accessible to the CI identity. The script then validates and applies `anubis.yaml`, restarts Anubis so policy-only changes take effect, and waits up to five minutes for the rollout. ## Validate locally ```sh ./test.sh ``` This checks both shell scripts and validates the Kubernetes resources with kubeconform. Traefik resources are skipped by schema validation because the standard catalog does not contain the cluster-specific CRDs; the Kubernetes API validates them during the deployment's server-side dry-run. ## Automatic deployment with Gitea Actions The workflow validates every push and pull request. A successful push to `main` applies `anubis.yaml` and waits up to five minutes for Anubis to become ready. CI substitutes the Git commit SHA into the pod-template annotation, so a ConfigMap-only policy edit starts a new pod instead of leaving the old policy in memory. The workflow requires one Gitea Actions repository secret: - `KUBE_CONFIG_BASE64`: a base64-encoded, restricted Anubis deployment kubeconfig. ### One-time CI setup 1. Run `./install.sh` with an administrator identity so all application resources already exist. 2. Run: ```sh ./create-ci-kubeconfig.sh ``` If kubectl is available only through arschrock, use remote mode: ```sh KUBECTL_SSH_HOST=arschrock ./create-ci-kubeconfig.sh ``` 3. Copy only the final base64 line into the Gitea repository Actions secret named `KUBE_CONFIG_BASE64`. 4. Protect `main`, require successful validation before merging, and review manifest and workflow changes. 5. Push a harmless README change and confirm that the deployment job succeeds. The workflow matches the existing brunner.ninja runner: Kubernetes API `https://host.containers.internal:6443`, with TLS server name `192.168.0.2`. CI can get, patch, and update only these existing named objects in `default`: - ConfigMap `gitea-anubis-policy`; - Deployment and Service `gitea-anubis`; - Middleware `gitea-anubis`; - IngressRoute `code-brunner-ninja-anubis`. It cannot create or delete resources, cannot access the Kubernetes Secrets API, and has no permission over `legacy-gitea-service`. Revoke CI access by deleting `default/gitea-anubis-deployer-token` and the Gitea repository secret. Rerun `create-ci-kubeconfig.sh` to issue a replacement token. Kubernetes RBAC cannot constrain individual fields inside an allowed Deployment patch. A malicious change merged into `main` could alter the pod specification, so branch protection and review remain part of the security boundary. ## Gitea robots.txt Install `robots.txt` as `/var/lib/gitea/custom/public/robots.txt` on arschrock, owned by `gitea:gitea`, then restart Gitea. Gitea requires a restart to load new custom public files. This host-level file is documented in the repository but is not changed by CI. ## Scope maintenance The mirror-owner list was generated from Gitea's public repository API on 2026-08-31. Add newly created third-party mirror owners to both `robots.txt` and the two owner expressions in `anubis.yaml`. Public `feedc0de` forks at or above the documented size threshold must be added to both their specific route expressions and `robots.txt`. Comparison pages are matched generically and need no owner-list maintenance.