2 Commits
Author SHA1 Message Date
florian.wilde 75077f9101 Merge branch 'phase/030-gitea-abgleich-2' 2026-10-12 17:13:18 +02:00
florian.wildeandClaude Opus 5.5 87b733cc9f chore(gitea): Issues nach Gitea, Basic-Auth fuer Konto-Passwort (#030)
In den Git-Zugangsdaten steht das Konto-Passwort, kein API-Token. Gitea
nimmt es nur per Basic-Auth an; das Skript erkennt das jetzt selbst.
Alle 30 Issues mit Labels und Meilensteinen uebertragen. Neu: #031
Rotation des Deployment-Keys und #032 Nacharbeiten fuer 1.0.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 17:13:15 +02:00
4 changed files with 71 additions and 12 deletions
+6 -4
View File
@@ -1,19 +1,21 @@
# #030 Issues nach Gitea übertragen
**Status:** offen
**Status:** erledigt
**Meilenstein:** v0.1.0
**Labels:** chore
**Geschätzt:** 1 h **Tatsächlich:** –
**Geschätzt:** 1 h **Tatsächlich:** 0 h 10 min
## Ziel
Issue-Dateien, Labels und Meilensteine in Gitea anlegen (wartet auf Zugangsdaten).
## Aufgaben
- [ ] `scripts/gitea-issues.ts`
- [x] `scripts/gitea-issues.ts`
## Akzeptanzkriterien
- [ ] wird zu Beginn der Phase verfeinert
- [x] wird zu Beginn der Phase verfeinert
## Abhängigkeiten
–
## Notizen / Entscheidungen
- Repo `florian.wilde/sshkeyvault` auf git.wilde-server.de (von Florian angelegt), `main` und Tags v0.1.0–v0.5.0 gepusht.
- `pnpm issues:sync` liest Adresse und Repo aus dem Git-Remote und den Token aus den Git-Zugangsdaten; die Issue-Dateien bleiben die Quelle der Wahrheit.
+25
View File
@@ -0,0 +1,25 @@
# #031 Rotation des Deployment-Keys
**Status:** offen
**Meilenstein:** v1.0.0
**Labels:** sicherheit, backend
**Geschätzt:** 3 h **Tatsächlich:** –
## Ziel
Den Deployment-Key der Instanz geführt tauschen – regelmäßig oder im Notfall (sshkv kompromittiert, siehe `docs/betrieb/sicherheit.md`).
## Aufgaben
- [ ] Neuen Deployment-Key erzeugen, ohne den alten abzulösen (zweiter Instanz-Key im Übergang)
- [ ] Auf jedem eingerichteten Host den neuen Key neben den alten in den verwalteten Block des Anmeldebenutzers schreiben (Dry-Run, Ausrollen)
- [ ] Verbindung mit dem neuen Key je Host prüfen
- [ ] Erst wenn alle Hosts den neuen Key akzeptieren: alten aus allen Blöcken entfernen, alten Key verwerfen
- [ ] Nicht erreichbare Hosts sichtbar machen; Abschluss erst, wenn alle umgestellt sind (oder bewusst übergangen)
- [ ] Nur Admin mit frischer Bestätigung, alles protokolliert
## Akzeptanzkriterien
- [ ] Integrationstest: nach der Rotation meldet sich sshkv nur noch mit dem neuen Key an, der alte steht auf keinem Host mehr
- [ ] Aussperrschutz: zu keinem Zeitpunkt steht ein Host ohne funktionierenden Deployment-Key da
## Abhängigkeiten
#016, #021
## Notizen / Entscheidungen
+22
View File
@@ -0,0 +1,22 @@
# #032 Nacharbeiten aus Phase 4/5
**Status:** offen
**Meilenstein:** v1.0.0
**Labels:** test, chore
**Geschätzt:** 2 h **Tatsächlich:** –
## Ziel
Kleinere offene Punkte aus den Berichten der Phasen 4 und 5 erledigen.
## Aufgaben
- [ ] Massenwiderruf im Browser testen (E2E mit frischer Bestätigung über den Mock-OIDC)
- [ ] Laufende Rotation abbrechen können (Nachfolger widerrufen, Vorgänger wieder aktiv)
- [ ] Protokoll-IDs ohne Zweierschritte (Spalten-Default und Trigger ziehen beide `nextval`)
- [ ] „Zum Dry-Run“ aus der Zuweisungsmatrix direkt zur Dry-Run-Karte springen (Anker)
## Akzeptanzkriterien
- [ ] Alle Suiten grün
## Abhängigkeiten
#023, #024, #025, #027
## Notizen / Entscheidungen
+18 -8
View File
@@ -76,14 +76,15 @@ class Gitea {
constructor(
private readonly basis: string,
private readonly repo: string,
private readonly token: string,
/** Fertiger Wert fuer den Authorization-Header (`token …` oder `Basic …`). */
private readonly autorisierung: string,
) {}
private async anfrage<T>(pfad: string, optionen: RequestInit = {}): Promise<T> {
const antwort = await fetch(`${this.basis}/api/v1/repos/${this.repo}${pfad}`, {
...optionen,
headers: {
Authorization: `token ${this.token}`,
Authorization: this.autorisierung,
"Content-Type": "application/json",
...optionen.headers,
},
@@ -149,10 +150,12 @@ function ausRemote(): { basis: string; repo: string } | null {
}
/**
* Holt den Token aus den Git-Zugangsdaten (Schlüsselbund oder `~/.git-credentials`).
* So genügt ein einziger Token für Push und API – er muss nicht zusätzlich in der .env stehen.
* Holt die Zugangsdaten aus Git (Schlüsselbund oder `~/.git-credentials`), damit sie nicht
* zusätzlich in der .env stehen müssen. Hinterlegt ist dort ein API-Token oder – wie bei
* sshkeyvault – das Konto-Passwort; Gitea nimmt beides an, das Passwort aber nur per Basic-Auth
* (#030).
*/
function tokenAusGitCredentials(basis: string): string | null {
function zugangAusGitCredentials(basis: string): { benutzer: string; geheimnis: string } | null {
const host = new URL(basis).host;
// Erst die eingerichteten Helfer (z. B. Schlüsselbund), dann zusätzlich die Datei
@@ -167,7 +170,8 @@ function tokenAusGitCredentials(basis: string): string | null {
stdio: ["pipe", "pipe", "ignore"],
});
const passwort = /^password=(.*)$/m.exec(antwort);
if (passwort?.[1]) return passwort[1];
const benutzer = /^username=(.*)$/m.exec(antwort);
if (passwort?.[1]) return { benutzer: benutzer?.[1] ?? "", geheimnis: passwort[1] };
} catch {
// Nächste Möglichkeit versuchen.
}
@@ -186,8 +190,14 @@ async function main() {
const remote = ausRemote();
const basis = (process.env.GITEA_URL ?? remote?.basis ?? "").replace(/\/$/, "");
const repo = process.env.GITEA_REPO ?? remote?.repo ?? "";
const token = process.env.GITEA_TOKEN || (basis ? (tokenAusGitCredentials(basis) ?? "") : "");
const zugang = process.env.GITEA_TOKEN ? null : basis ? zugangAusGitCredentials(basis) : null;
const token = process.env.GITEA_TOKEN || zugang?.geheimnis || "";
const tokenQuelle = process.env.GITEA_TOKEN ? "Umgebungsvariable" : "Git-Zugangsdaten";
// Ein Gitea-Token sind 40 Hex-Zeichen; alles andere ist ein Passwort und geht per Basic-Auth.
const autorisierung =
process.env.GITEA_TOKEN || /^[0-9a-f]{40}$/.test(token) || !zugang?.benutzer
? `token ${token}`
: `Basic ${Buffer.from(`${zugang.benutzer}:${token}`).toString("base64")}`;
const verzeichnis = path.join(process.cwd(), "docs", "issues");
const issues = await dateienLesen(verzeichnis);
@@ -215,7 +225,7 @@ async function main() {
}
console.log(`Ziel: ${basis}/${repo} (Token aus: ${tokenQuelle})`);
const gitea = new Gitea(basis, repo, token);
const gitea = new Gitea(basis, repo, autorisierung);
const vorhandeneLabels = new Map((await gitea.labels()).map((l) => [l.name.toLowerCase(), l.id]));
const benoetigteLabels = [...new Set(issues.flatMap((issue) => issue.labels))];