# Hermes Host Git Remote Cutover to K8s Gitea — 2026-07-14

## Context

After ArgoCD self-management cutover (Section 4.3e), the Hermes host's
own `iac-homelab` git remote still pointed to CT108
(`10.0.30.105:3000`). User instructed: "nutze du intern in hermes ab
jetzt auch das k8s gitea" — switch Hermes host's git remote to K8s Gitea.

## Key Learnings

### CoreDNS Is K8s-Internal Only

The CoreDNS `hosts` override (Section 4.3d) makes `git.schoen.codes`
resolve to `10.0.30.203` **only inside K8s pods**. The Hermes host,
being outside K8s, cannot use CoreDNS. Public DNS was explicitly
rejected by the user ("Nutze für Internet Zwecke nicht den externen DNS
Resolver, sondern einen internen").

Solution: `/etc/hosts` entry on the Hermes host:
```bash
echo "10.0.30.203 git.schoen.codes" | sudo tee -a /etc/hosts
```

### Gitea API Tokens Do NOT Migrate

When repos are migrated from CT108 Gitea to K8s Gitea via the Gitea
Migration API, **API tokens are NOT copied**. Tokens are stored hashed
in the database and are instance-specific. The old token
(`07efa...`) from CT108 returns `401 Unauthorized` on K8s Gitea.

New token must be generated on K8s Gitea:
```bash
# Via mgmt-runner (VM200) — Hermes host has no kubectl:
ssh -i ~/.ssh/id_ed25519_proxmox root@10.0.30.124 \
  "KUBECONFIG=/root/.kube/config kubectl exec -n gitea deploy/gitea -- \
   gitea admin user generate-access-token \
   -u dominik -t hermes-gitops --scopes all --raw"
```

Note: The `--config /data/gitea/conf/app.ini` flag is optional —
`gitea admin user generate-access-token` auto-discovers the config when
running inside the pod. The command outputs the raw token to stdout.

### JWT ≠ Gitea API Token (Critical Distinction)

The 1Password ExternalSecrets for Gitea (`gitea-security`) contain
`internal_token`, `jwt_secret`, `secret_key`, `lfs_jwt_secret`. These
are **internal security tokens** (JWTs), NOT API access tokens.

**How to distinguish:**
- **Gitea API token**: hex string, e.g. `07efa534ea1a147fce1bf5813c669cb0ebc7b522`
- **JWT / internal security token**: three base64 parts separated by dots,
  e.g. `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE3ODM5NjM1NDd9.Lyr6fqc...`

A JWT passed as `Authorization: token <JWT>` or `Authorization: Bearer
<JWT>` returns `401 Unauthorized` on both old and new Gitea instances.
The `nbf` (Not Before) claim in the JWT payload identifies it as a
security token, not an access token.

**If a user provides a token that looks like a JWT (dots separating
base64 segments), it is NOT a Gitea API token.** Direct them to
Settings → Applications → Generate New Token in the Gitea web UI, or
generate one via `gitea admin user generate-access-token` (see above).

### K8s Gitea Reachability from Outside K8s

| Path | Works? | Notes |
|------|--------|-------|
| `http://10.0.30.203:3000` | ❌ Timeout | Port 3000 not exposed on VIP |
| `http://10.0.30.203` (Host: git.schoen.codes) | ✅ 200 | Traefik routes by Host header |
| `https://10.0.30.203` (Host: git.schoen.codes) | ✅ 200 | TLS via Traefik |
| `10.0.30.202:2222` (SSH) | Separate LB | Gitea SSH LoadBalancer service |
| `git.schoen.codes` (via /etc/hosts) | ✅ | Resolves to 10.0.30.203 |

### Hermes Host Has No kubectl

The Hermes host (`/home/debian`) does not have `kubectl`, `helm`, or
`kubeconfig`. All K8s operations go through SSH to mgmt-runner (VM200)
at `10.0.30.124`. See SKILL.md Section 1.2.

### ArgoCD Repo Secret Also Needs Token Update

After generating a new Gitea API token, the ArgoCD repository secret
must also be patched with the new token — otherwise ArgoCD's repo
connection breaks even though the Hermes host git remote works:

```bash
ssh -i ~/.ssh/id_ed25519_proxmox root@10.0.30.124 \
  "KUBECONFIG=/root/.kube/config kubectl patch secret \
   argocd-repo-k8s-gitea -n argocd --type merge \
   -p '{\"data\":{\"password\":\"$(echo -n TOKEN | base64)\"}}'"
```

### Gitea Helm Chart Admin Password Extraction

The Gitea Helm Chart v12 auto-generates an admin password on first
install (unless explicitly set in values). The password is stored as
the `GITEA_ADMIN_PASSWORD` environment variable in the `configure-gitea`
init container — NOT in any K8s Secret or Helm values file.

```bash
# Extract admin password from the init container env vars:
ssh -i ~/.ssh/id_ed25519_proxmox root@10.0.30.124 \
  "KUBECONFIG=/root/.kube/config kubectl get deploy gitea -n gitea \
   -o jsonpath='{.spec.template.spec.initContainers[?(@.name==\"configure-gitea\")].env}'" \
  | python3 -c "
import json,sys
for e in json.load(sys.stdin):
    if e.get('name')=='GITEA_ADMIN_PASSWORD':
        print(e['value'])
"
```

Additional env vars in the same container:
- `GITEA_ADMIN_USERNAME`: the admin username (e.g. `dominik`)
- `GITEA_ADMIN_PASSWORD_MODE`: `keepUpdated` (chart syncs password on
  every sync) or `initialOnlyRequireReset`

**Pitfall:** The password is NOT in the `gitea-security` K8s Secret
(that contains JWT-based internal tokens only — see "JWT ≠ Gitea API
Token" above). It is also NOT in the Helm values `gitea.admin` section
(unless explicitly set — by default the chart generates a random one).

### Storing Credentials in 1Password

After extracting the admin password and generating an API token, store
both in 1Password vault "Hermes":

- **API Token**: Category `API Credential`, title `K8s Gitea Token
  (hermes-gitops)`. Requires two-step create+edit (see 1password-cli
  skill — `op item create` does not populate the `credential` field
  for API Credential items).
- **Admin Login**: Category `Login`, title `K8s Gitea Admin (dominik)`.
  Works in one step with `op item create --category="Login"`.

## Steps Performed (Completed)

1. Checked current remote: `git remote -v` → CT108 (`10.0.30.105:3000`)
2. Verified K8s Gitea reachable via Traefik VIP: `curl -H "Host: git.schoen.codes" http://10.0.30.203/` → 200
3. Added `/etc/hosts` entry: `10.0.30.203 git.schoen.codes`
4. Changed git remote URL to `http://dominik:TOKEN@git.schoen.codes/dominik/iac-homelab.git`
5. `git fetch origin` → `Authentication failed` (old CT108 token invalid on K8s Gitea)
6. User provided a JWT (internal security token from 1Password) — identified as NOT an API token (see pitfall above)
7. Generated new API token via mgmt-runner SSH: `gitea admin user generate-access-token -u dominik -t hermes-gitops --scopes all --raw` → `4a3fd...`
8. Updated git remote with new token: `git remote set-url origin "http://dominik:4a3fd...@git.schoen.codes/..."`
9. `git fetch origin` → ✅ success (forced update from `d9135ca` → `68adfab`)
10. `git push --dry-run origin main` → ✅ "Everything up-to-date"
11. Patched ArgoCD repo secret `argocd-repo-k8s-gitea` with new token

## Result

Hermes host git operations now use K8s Gitea exclusively. ArgoCD also
pulls from K8s Gitea. The entire GitOps loop is self-contained in K8s.
CT108 can be decommissioned (Task 10 — pending user approval).
