Przejdź do treści
DevOps notes
← Wróć do notatek

Self-hosted Forgejo - osobisty serwer Git.

Ostatnio GitHub nie ma dobrej prasy: stabilność pod znakiem zapytania, poważne podatności, a do tego nikt nie może być w stu procentach pewny, że jego kod nie trafia do treningu modeli. Można oczywiście wybrać inny saas, Bitbucket, Gitlab, Gitea czy Codeberg, ale to wciąż wysyłanie kodu do dostawców chmurowych. Alternatywą jest serwis self-hosted, wcale nie taki trudny do zrealizowania, przynajmniej dla użytku prywatnego lub mniejszych zespołów.

Proponowany setup to jedna maszyna wirtualna w Hetznerze z Forgejo uruchomionym przez docker-compose, z deploymentem przez Pulumi w Go. Ruch HTTPS od strony klienta jest proxowany przez Cloudflare, a po stronie serwera połączenie z Cloudflare zabezpiecza Caddy. Dane są co noc backupowane (z szyfrowaniem przez restic) do bucketa zgodnego z S3 (w tym przypadku AWS).

Kod dla infrastruktury

Deployment w całości zapewnia Pulumi, ale rozdzielony jest na dwie części: niezmienną infrastrukturę i konfigurację aplikacji.

Infrastruktura to VM-ka w serwisie Hetzner (domyślnie cx23), firewall i klucz SSH. Konfiguracja aplikacji to "części ruchome": obraz kontenera, domena, ustawienia Forgejo i opcjonalnie backupu. Cloud-init w UserData (bootstrap.yaml) ma jedynie za zadanie instalację i uruchomienie Dockera. Jest celowo statyczny i przypięty w kodzie przez ignoreChanges, bo każda jego zmiana wymusza przebudowę maszyny.

Sama konfiguracja Forgejo jest renderowana z wartości Pulumi do pliku docker-compose.yml i wypychana po SSH przez provider command.remote, który uruchamia na serwerze docker compose up -d.

Klucz SSH i token do Hetznera żyją zaszyfrowane w stacku (Pulumi Cloud), więc pulumi up działa z dowolnej maszyny - bez trzymania sekretów lokalnie w plikach.

Konfiguracja Forgejo

Świeża instancja Forgejo domyślnie wystawia kreator instalacji, dlatego na potrzeby tego deploymentu cała konfiguracja zawarta jest w zmiennych środowiskowych dla docker-compose, a Forgejo wstaje od razu skonfigurowane:

Ustawienie Domyślnie Efekt
installLock true Blokuje kreator instalacji
disableRegistration true Brak samodzielnej rejestracji użytkowników
requireSigninView false Publiczne repozytoria czytelne bez logowania
adminUsername / adminEmail Konto admina tworzone automatycznie

Hasło admina jest generowane na serwerze i zapisywane do pliku, który odczytujesz po SSH przy pierwszym logowaniu (i tak zostaniesz zmuszony do jego zmiany).

HTTPS: Caddy za Cloudflare

Kod Pulumi nie zawiera ustawień Cloudflare, do wybranej domeny trzeba dodać rekord DNS wskazujący na IP instancji w Hetzner (w trybie proxy). Dla maksymalnego zabezpieczenia origina, Caddy wymaga dostarczenia Origin CA Certificate od Cloudflare (cert i klucz prywatny dostarczone są przez sekrety Pulumi) oraz ustawienia TLS po stronie Cloudflare w trybie Full Strict. Bezpośredni dostęp do Forgejo przez IP instancji jest zablokowany na poziomie firewalla, który na portach webowych wpuszcza jedynie opublikowane zakresy IP Cloudflare.

Dostęp do gita tylko po HTTPS

Dostęp do serwera Git jedynie przez proxy Cloudflare ukrywa IP origina i uniemożliwia klonowanie repozytoriów przez SSH, jednak HTTPS z tokenem spełnia swoje zadanie.

Backup: restic → S3

Backup jest opcjonalny i włącza się przez ustawienie repozytorium S3 oraz sekretów. Skrypt odpala backup SQLite przez Online Backup API, co pozwala na kopiowanie bez konieczności zatrzymywania bazy. Kopia jest następnie częścią szyfrowanego snapshota restic (razem z plikami repozytoriów, LFS, załącznikami i plikiem app.ini). Każdy backup jest wart tyle, co jego przetestowane odzyskanie, dlatego Pulumi pozwala na szybki restore - wystarczy dodać w tym samym repozytorium kolejny stack Pulumi, który stawia obok odrębną instancję Forgejo. Sama procedura instalacji snapshotów z restic jest opisana w README.md. Mechanizm blue-green przydaje się również do aktualizacji serwera - ewentualne problemy z nową wersją nie wpływają na działającą instancję.

Bezpieczeństwo

Największym zabezpieczeniem jest tu ograniczona powierzchnia ataku, a nie łatanie pojedynczych dziur. Rejestracja jest wyłączona (disableRegistration=true), więc konta zakłada wyłącznie admin. W tym setupie requireSigninView jest ustawione na false, dzięki czemu publiczne repozytoria są czytelne bez logowania - anonimowy ruch widzi więc publiczne repo, stronę logowania i część API, ale nie może założyć konta ani sięgnąć do repozytoriów prywatnych. Listy użytkowników i organizacji są dodatkowo ukryte (disableUsersPage, disableOrganizationsPage). Przed originem stoi Cloudflare (WAF, rate limiting, ukrycie IP), a firewall wpuszcza na porty webowe wyłącznie zakresy IP Cloudflare - proxy nie da się ominąć.

Dodatkowe warstwy: dostęp SSH zawężony do własnego IP (albo schowany za VPN), git wyłącznie po HTTPS z zakresowym tokenem, globalne wymuszenie MFA. Dla maksymalnego zabezpieczenia całość można schować za Cloudflare Access lub VPN, tak by nawet strona logowania nie była publicznie osiągalna. Hasło restic i SECRET_KEY (z app.ini) trzymane są poza serwerem, żeby przejęcie maszyny nie oznaczało utraty backupów.

Podsumowanie

Proponowane rozwiązanie jest moim zdaniem wystarczające do użytku osobistego - pozwala na hostowanie zarówno prywatnych, jak i publicznych repozytoriów. Kompletną instalację, razem z ustawieniami Cloudflare, można wykonać w 30 minut, a koszt instancji cx23 to kilka euro miesięcznie.

Kod deploymentu znajdziesz tutaj.