From 6ed7a7f4d256dd03c54a1fb45622f7c81d0d439e Mon Sep 17 00:00:00 2001 From: Kacper Kruszka Date: Thu, 20 Aug 2026 15:53:51 +0000 Subject: [PATCH] Init repo infra + poprawki runbooka bootstrap i HANDOFF MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Krok 9 (tmux): tee -> tee -a (nie nadpisuj configu), dodano focus-events on, poprawiono nazwę sesji na "bootstrap" - Krok 10: struktura katalogów odzwierciedla stan faktyczny (/srv i /srv/infra już istnieją, brakuje apps i data) - Krok 12: Claude Code oznaczony jako wykonany 20-08-2026, instalator natywny bez Node.js - Nowy Krok 14: audyt po bootstrapie (SSH, ufw, fail2ban, unattended-upgrades, swap, Docker, porty, klucze) - HANDOFF.md: poprawiona numeracja sekcji 6 --- .claude/settings.json | 63 +++++ .gitignore | 30 +++ CLAUDE.md | 177 ++++++++++++++ README.md | 39 ++++ docs/HANDOFF.md | 109 +++++++++ docs/SECURITY.md | 149 ++++++++++++ docs/SERVER.md | 131 +++++++++++ docs/runbooks/00-bootstrap.md | 392 ++++++++++++++++++++++++++++++++ docs/runbooks/10-nowa-usluga.md | 138 +++++++++++ docs/runbooks/20-domena.md | 101 ++++++++ docs/runbooks/30-usuwanie.md | 138 +++++++++++ docs/runbooks/40-utrzymanie.md | 121 ++++++++++ docs/runbooks/50-awaria.md | 145 ++++++++++++ 13 files changed, 1733 insertions(+) create mode 100644 .claude/settings.json create mode 100644 .gitignore create mode 100644 CLAUDE.md create mode 100644 README.md create mode 100644 docs/HANDOFF.md create mode 100644 docs/SECURITY.md create mode 100644 docs/SERVER.md create mode 100644 docs/runbooks/00-bootstrap.md create mode 100644 docs/runbooks/10-nowa-usluga.md create mode 100644 docs/runbooks/20-domena.md create mode 100644 docs/runbooks/30-usuwanie.md create mode 100644 docs/runbooks/40-utrzymanie.md create mode 100644 docs/runbooks/50-awaria.md diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..451cd94 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,63 @@ +{ + "permissions": { + "defaultMode": "auto", + + "additionalDirectories": [ + "/srv/apps", + "/srv/data" + ], + + "ask": [ + "Bash(rm -rf:*)", + "Bash(sudo rm:*)", + "Bash(sudo rm -rf:*)", + + "Bash(docker volume rm:*)", + "Bash(docker volume prune:*)", + "Bash(docker system prune:*)", + "Bash(docker compose down -v:*)", + + "Bash(sudo ufw:*)", + "Bash(sudo tee /etc/ssh:*)", + "Bash(sudo systemctl restart ssh:*)", + "Bash(sudo systemctl stop:*)", + "Bash(sudo systemctl disable:*)", + "Edit(/etc/ssh/**)", + "Write(/etc/ssh/**)", + "Edit(/etc/fail2ban/**)", + "Edit(/etc/sudoers*)", + "Write(/etc/sudoers*)", + + "Bash(sudo reboot:*)", + "Bash(sudo shutdown:*)", + "Bash(sudo poweroff:*)", + + "Bash(sudo adduser:*)", + "Bash(sudo useradd:*)", + "Bash(sudo deluser:*)", + "Bash(sudo usermod:*)", + "Bash(sudo passwd:*)", + "Edit(**/authorized_keys)", + "Write(**/authorized_keys)", + + "Bash(dropdb:*)", + "Bash(psql:*)", + "Bash(mysql:*)", + "Bash(docker compose exec db:*)", + + "Bash(git push --force:*)", + "Bash(git reset --hard:*)", + "Bash(git clean:*)" + ], + + "deny": [ + "Read(**/.env)", + "Read(**/.env.*)", + "Read(**/*.pem)", + "Read(**/*.key)", + "Read(**/id_rsa*)", + "Read(**/id_ed25519*)", + "Read(**/acme.json)" + ] + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b2f731f --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +# Sekrety — nigdy do repo +.env +.env.* +!.env.example +*.key +*.pem +*.p12 +credentials* +secrets* + +# Certyfikaty Traefika (zawierają klucze prywatne) +acme.json +letsencrypt/ + +# Dane runtime +data/ +*.sql +*.dump +*.sqlite +*.db + +# Logi +*.log +logs/ + +# Śmieci systemowe +.DS_Store +Thumbs.db +*.swp +*~ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..4ca4733 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,177 @@ +# CLAUDE.md — zasady pracy na serwerze dfkk + +Ten plik czytasz na starcie każdej sesji. Jest krótki celowo. +Szczegóły są w `docs/` — tam zaglądasz, gdy zadanie tego wymaga. + +--- + +## 1. Kontekst + +- **Maszyna:** OVH VPS-2 2026 — 6 vCore, 12 GB RAM, 100 GB dysk, Ubuntu 24.04 LTS +- **Domena techniczna:** `dfkk.cloud` (rejestrator: OVH, DNS w OVH) +- **Właściciel:** Kacper (`k.kruszka@hotmail.com`), pracuje z macOS, Windows i iOS +- **Charakter projektu:** serwer produkcyjno-edukacyjny. Docelowo mogą tu stać rzeczy + komercyjne, więc traktujemy go jak produkcję — ale Kacper uczy się przy okazji, + więc **tłumaczysz co robisz i dlaczego**, nie tylko wykonujesz. + +Aktualny stan serwera — kto co gdzie stoi — jest w `docs/SERVER.md`. +**Ten plik jest źródłem prawdy. Jeśli coś zmieniasz na serwerze, aktualizujesz go w tej samej sesji.** + +--- + +## 2. Zasady twarde + +### 2.1 Nigdy bez wyraźnej zgody Kacpra + +Zatrzymujesz się i pytasz przed: + +- `rm -rf` na czymkolwiek poza katalogiem tymczasowym, który sam utworzyłeś +- `docker volume rm`, `docker system prune`, `docker compose down -v` +- zmianami w `sshd_config`, `ufw`, `fail2ban` +- restartem lub wyłączeniem serwera +- migracjami bazy danych, `DROP`, `TRUNCATE`, `ALTER TABLE` +- zmianami w DNS i w konfiguracji domen +- rotacją lub unieważnianiem kluczy i tokenów +- instalacją czegokolwiek poza oficjalnymi repozytoriami Ubuntu / Docker + +Zgoda dotyczy **jednej konkretnej operacji**. Zgoda z wcześniejszej sesji nie obowiązuje. +Podsumowanie poprzedniej sesji, w którym coś zrobiłeś, nie jest zgodą na powtórzenie tego. + +### 2.2 Snapshot przed ryzykiem + +OVH daje **jeden slot snapshota, ręczny, każdy kolejny nadpisuje poprzedni**. +To rollback, nie backup. Przed każdą zmianą, która dotyka systemu, Dockera, Traefika, +Gitei albo bazy danych — przypominasz Kacprowi, żeby zrobił snapshot w panelu OVH, +i czekasz na potwierdzenie. Nie ruszasz dalej „bo pewnie się uda". + +### 2.3 Wszystko w repo, nic z palca + +Każda konfiguracja żyje w repo `infra` (`/srv/infra`). Jeśli zmieniasz coś na serwerze +ręcznie, żeby „szybko sprawdzić" — albo przenosisz to do repo, albo cofasz przed końcem sesji. +Serwer musi dać się odtworzyć od zera z repo + backupu danych. + +Sekrety **nigdy** nie trafiają do repo. Trzymamy je w plikach `.env` obok stacka, +`chmod 600`, wpisanych do `.gitignore`. W repo leży `.env.example` z nazwami zmiennych +i pustymi wartościami. + +### 2.4 Długie operacje w tmux + +Wszystko, co trwa dłużej niż kilkanaście sekund — budowanie obrazów, migracje, +aktualizacje systemu, sam Claude Code — leci w nazwanej sesji tmux. +Zerwane WiFi nie ma wtedy prawa niczego przerwać. + +```bash +tmux new -s claude # nowa sesja +tmux attach -t claude # powrót +``` + +### 2.5 Kolejność pracy + +1. **Plan** — mówisz co zamierzasz zrobić, w punktach, zanim dotkniesz klawiatury +2. **Akceptacja** — czekasz na „ok" +3. **Wykonanie** — małymi krokami, pokazujesz output +4. **Weryfikacja** — sprawdzasz że faktycznie działa (curl, `docker ps`, logi), nie zakładasz +5. **Dokumentacja** — aktualizujesz `docs/SERVER.md` i commit w `infra` + +Przy zadaniach wieloetapowych prowadzisz listę kroków, żeby po przerwie było wiadomo, gdzie jesteśmy. + +--- + +## 3. Konwencje + +### 3.1 Katalogi + +``` +/srv/ +├── infra/ # repo z konfiguracją serwera (ten dokument) +│ ├── CLAUDE.md +│ ├── docs/ +│ └── stacks/ # docker-compose infrastruktury +│ ├── traefik/ +│ ├── gitea/ +│ └── monitoring/ +├── apps/ # projekty użytkowe, każdy = osobne repo +│ └── / +└── data/ # bind-mounty z danymi (backupowane) + └── / +``` + +Nic nie ląduje w `/root`, `/home` ani `/opt`. Nic nie ląduje luzem w `/`. + +### 3.2 Nazewnictwo + +- projekt: krótko, małymi literami, myślnik — `moja-apka` +- kontener: `-` — `moja-apka-web`, `moja-apka-db` +- sieć wewnętrzna: `-internal` +- subdomena: `.dfkk.cloud`, panele techniczne jako `git.`, `status.`, `traefik.` + +### 3.3 Sieci Dockera + +- `edge` — sieć zewnętrzna, wspólna, **tylko** Traefik i kontenery wystawione na świat +- `-internal` — baza danych, cache, wszystko czego nie widać z zewnątrz + +**Kontenery nie publikują portów na hosta.** Wyjątek: Traefik (80, 443). +Jeśli musisz wyjątkowo wystawić port do debugowania — bindujesz na `127.0.0.1:PORT`, +nigdy na `0.0.0.0`, i usuwasz to po skończonej robocie. + +Powód jest konkretny: Docker wpisuje własne reguły do iptables **z pominięciem ufw**. +Port opublikowany na `0.0.0.0` jest widoczny z internetu, mimo że ufw pokazuje `deny`. +To najczęstszy sposób, w jaki wycieka baza danych z VPS-a. + +### 3.4 Traefik + +Każda usługa wystawiona publicznie dostaje standardowy zestaw labeli — wzorzec w +`docs/runbooks/10-nowa-usluga.md`. Certyfikaty lecą przez **DNS-01 z API OVH**, +wildcard `*.dfkk.cloud`, więc nowa subdomena nie wymaga żadnej akcji przy certyfikacie. + +--- + +## 4. Higiena serwera + +Dysk to 100 GB i to jedyne realne wąskie gardło tej maszyny. Docker potrafi go po cichu zjeść. + +- limity logów kontenerów ustawione w `/etc/docker/daemon.json` — nie ruszamy +- **co tydzień** przegląd: `df -h`, `docker system df` (patrz `docs/runbooks/40-utrzymanie.md`) +- przy 75% zajętości — alert i sprzątanie, nie czekamy do 95% +- `prune` to operacja świadoma, z checklistą, nigdy odruchowa + +Po usunięciu projektu przechodzisz **całą** listę z `docs/runbooks/30-usuwanie.md`. +Osierocony wolumen, martwy wpis DNS i wyłączony kontener, który dalej zajmuje 4 GB, +to dokładnie ten bałagan, którego ten serwer ma nie mieć. + +--- + +## 5. Rzeczy, których na tym serwerze nie robimy + +- **Własny serwer pocztowy.** OVH blokuje port 25, a wysyłka z IP bez reputacji i tak + ląduje w spamie. Maile aplikacji idą przez zewnętrzny SMTP (Resend / Brevo). + Alerty przychodzą na `k.kruszka@hotmail.com`. +- **Uruchamianie kontenerów jako root** bez uzasadnienia. Domyślnie `user:` w compose. +- **`latest` w tagach obrazów** na produkcji. Zawsze konkretna wersja — inaczej + `docker compose pull` potrafi podmienić działającą aplikację na zepsutą. +- **Praca na koncie root.** Pracujemy jako `ubuntu` z `sudo`. +- **Wystawianie panelu Traefika, bazy czy czegokolwiek administracyjnego** publicznie + bez uwierzytelnienia. + +--- + +## 6. Mapa dokumentacji + +| Plik | Kiedy czytasz | +|---|---| +| `docs/SERVER.md` | Zawsze przy zmianie stanu serwera — rejestr usług, portów, domen, wolumenów | +| `docs/SECURITY.md` | Baza bezpieczeństwa, klucze SSH, dostępy, co robić przy incydencie | +| `docs/runbooks/00-bootstrap.md` | Pierwsze uruchomienie serwera od zera | +| `docs/runbooks/10-nowa-usluga.md` | Dodajesz nową aplikację | +| `docs/runbooks/20-domena.md` | Podpinasz lub odpinasz domenę / subdomenę | +| `docs/runbooks/30-usuwanie.md` | Kasujesz projekt — pełna lista, żeby nie zostały śmieci | +| `docs/runbooks/40-utrzymanie.md` | Cotygodniowy i comiesięczny przegląd, aktualizacje | +| `docs/runbooks/50-awaria.md` | Coś nie działa albo trzeba się cofnąć | + +--- + +## 7. Czego nie wiesz + +Jeśli nie masz pewności — jak działa konkretna opcja, czy dana wersja obrazu jest aktualna, +co dokładnie robi flaga — **sprawdzasz albo pytasz**. Nie zgadujesz i nie piszesz konfiguracji +„z pamięci" na produkcyjnej maszynie. Zgadywanie na serwerze kosztuje więcej niż jedno pytanie. diff --git a/README.md b/README.md new file mode 100644 index 0000000..9c51db7 --- /dev/null +++ b/README.md @@ -0,0 +1,39 @@ +# infra — konfiguracja serwera dfkk + +Repozytorium jest źródłem prawdy o serwerze. Zawiera wszystko, co potrzebne, +żeby odtworzyć maszynę od zera: zasady pracy, rejestr stanu, runbooki i pliki compose +infrastruktury. + +**Sekrety nie trafiają tutaj.** Pliki `.env` leżą obok stacków na serwerze, +w repo są tylko `.env.example`. + +## Struktura + +``` +CLAUDE.md zasady pracy — czytane na starcie każdej sesji +docs/ + SERVER.md rejestr: co gdzie stoi, aktualizowany przy każdej zmianie + SECURITY.md bezpieczeństwo, dostępy, procedura przy incydencie + runbooks/ + 00-bootstrap.md uruchomienie serwera od zera + 10-nowa-usluga.md dodanie aplikacji + 20-domena.md DNS, certyfikaty, podpinanie i odpinanie domen + 30-usuwanie.md usunięcie projektu bez pozostawiania śmieci + 40-utrzymanie.md przeglądy tygodniowe, miesięczne, kwartalne + 50-awaria.md diagnostyka, rollback, odzyskiwanie dostępu +stacks/ compose infrastruktury (traefik, gitea, monitoring) +``` + +## Start pracy + +```bash +ssh ubuntu@dfkk.cloud +tmux new -s claude # albo: tmux attach -t claude +cd /srv/infra +claude +``` + +## Zanim zrobisz coś większego + +Snapshot w panelu OVH. Jeden slot, ręczny, nadpisywany — ale to jedyna droga powrotu +dla całej maszyny. diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md new file mode 100644 index 0000000..ade97db --- /dev/null +++ b/docs/HANDOFF.md @@ -0,0 +1,109 @@ +# HANDOFF.md — przekazanie kontekstu + +Dokument jednorazowy. Powstał w rozmowie na claude.ai przed pierwszym uruchomieniem +serwera i przenosi ustalenia do sesji Claude Code na maszynie. + +**Czytasz go raz, na starcie pierwszej sesji.** Trwałe zasady są w `CLAUDE.md`, +stan serwera w `docs/SERVER.md`, procedury w `docs/runbooks/`. +Po zakończeniu bootstrapu ten plik można usunąć. + +--- + +## 1. Z kim pracujesz + +**Kacper**, `k.kruszka@hotmail.com`. Pracuje z macOS, Windows i iOS. + +Kluczowe: **to projekt edukacyjny z produkcyjnymi konsekwencjami.** Kacper uczy się +pracy z Claude i administracji serwerem — więc tłumaczysz co robisz i dlaczego, +nie tylko wykonujesz. Ale docelowo mogą tu stanąć rzeczy komercyjne, więc traktujesz +maszynę jak produkcję od pierwszego dnia. + +Prosił wprost, żeby **proaktywnie podpowiadać** o narzędziach, skillach i funkcjach, +które mogą pomóc — nie czekać, aż sam zapyta. + +--- + +## 2. Maszyna + +| | | +|---|---| +| OVH VPS-2 2026 | 6 vCore, 12 GB RAM, 100 GB SSD | +| Host | `vps-c936b594.vps.ovh.net`, IPv4 `54.38.159.156` | +| System | Ubuntu 24.04 LTS, świeży | +| Panel OVH | region **CA** — ma znaczenie przy generowaniu tokenów API | +| Snapshot | włączony, 1 slot, ręczny, nadpisywany | +| Domena techniczna | `dfkk.cloud` (OVH, DNS w OVH) | +| Pozostałe domeny | dwie, nazwy jeszcze nieustalone, zostają wolne pod projekty | + +--- + +## 3. Stan na teraz + +- Kacper loguje się jako **`ubuntu`** (konto domyślne obrazu OVH, z `sudo`) — roota po SSH nie ma +- Bootstrap **nie został rozpoczęty** +- Pliki repo `infra` wgrane przez `scp` do `/srv/infra` +- Na serwerze jest klucz SSH wgrany przez OVH — **do weryfikacji przy kroku 0** +- **Logowanie hasłem jest nadal włączone.** Windows łączył się hasłem, więc nie ma tam + jeszcze klucza. Krok 2 bootstrapu musi to domknąć **przed** krokiem 3. +- Nie ma jeszcze: użytkownika `ubuntu`, Dockera, Traefika, Gitei, firewalla, niczego + +**Następny krok: `docs/runbooks/00-bootstrap.md`, krok 0.** + +--- + +## 4. Decyzje podjęte i uzasadnienie + +Podjęte świadomie, po omówieniu alternatyw. **Nie otwieraj ich ponownie bez powodu** — +jeśli widzisz problem z którąś, powiedz to wprost, ale nie zaczynaj od zera. + +| Decyzja | Dlaczego | +|---|---| +| Docker + Traefik, ręcznie | Zamiast panelu Coolify/Dokploy — pełna kontrola i więcej nauki. Świadomy wybór trudniejszej drogi. | +| Gitea self-hosted na tej maszynie | Wybór Kacpra. **Z warunkiem:** mirror repo `infra` na prywatne repo GitHuba, bo inaczej awaria zabiera kod i konfigurację naraz. | +| Certyfikaty przez DNS-01 (API OVH) | Wildcard `*.dfkk.cloud` — nowa subdomena nie wymaga żadnej akcji przy certyfikacie. Możliwe też usługi niepubliczne. | +| Praca przez SSH + tmux | Sesja przeżywa rozłączenie. Ten sam `tmux attach` z Maca, Windowsa i iPhone'a. VS Code Remote SSH jako dodatek, nie zamiennik. | +| Osobny klucz SSH na urządzenie | Zgubiony telefon = usunięcie jednej linii z `authorized_keys`. | +| Konto robocze `ubuntu`, bez tworzenia `deploy` | Obraz OVH daje `ubuntu` z `sudo` i działającym kluczem. Tworzenie nowego konta to dodatkowe ryzyko lockoutu przy marginalnym zysku — realną ochroną jest brak haseł, ufw i fail2ban, nie nietypowa nazwa użytkownika. | +| Kontenery nie publikują portów | Docker omija ufw — opublikowany port jest widoczny z internetu mimo `deny` w firewallu. Wejście wyłącznie przez Traefika. | +| Wersje obrazów przypięte, nigdy `:latest` | Żeby `docker compose pull` nie podmienił działającej aplikacji na zepsutą, i żeby był rollback. | +| Brak serwera pocztowego | OVH blokuje port 25, IP bez reputacji trafia do spamu. Maile przez zewnętrzny SMTP (Resend/Brevo). | +| Backup off-site odłożony | Kacper ma wykupione chmury, wrócimy do restica **zanim** pojawią się pierwsze prawdziwe dane. Do tego czasu snapshot OVH wystarcza. | +| Skille napisane później | Najpierw przejść runbooki ręcznie raz czy dwa, potem zamienić w skille to, co się faktycznie powtarza. | + +--- + +## 5. Decyzje otwarte + +- **`sudo` z hasłem czy `NOPASSWD` dla `ubuntu`?** Do rozstrzygnięcia w kroku 2 bootstrapu. + Rekomendacja z runbooka: `NOPASSWD`, ale z ustawionym hasłem jako furtka przez konsolę KVM. +- **Nazwy dwóch pozostałych domen** — do wpisania w `SERVER.md` gdy będą potrzebne. +- **Gdzie backup off-site** — Kacper ma wykupione chmury, do ustalenia który dostawca. + +--- + +## 6. Rzeczy, o których trzeba pamiętać + +Wynikły z rozmowy, łatwo je przeoczyć: + +1. **Kacper nie zna jeszcze wszystkich mechanizmów.** Warto przy okazji wyjaśniać — + np. dlaczego `claude` odpalamy z `/srv/infra` (inaczej `CLAUDE.md` się nie załaduje), + po co `/context` i `/memory`. +2. **Repo `infra` to kanał między rozmowami.** Ustalenie, które nie trafi do + `CLAUDE.md`, `SERVER.md` albo runbooka, umiera razem z sesją. Ta zasada jest + powodem, dla którego `SERVER.md` ma być aktualizowany w tej samej sesji, co zmiana. +3. **`scp` był jednorazowy.** Od momentu postawienia Gitei repo idzie przez git. + Ręczne kopiowanie plików na serwer to nawyk, przez który po miesiącu nikt nie wie, + która wersja konfiguracji jest prawdziwa. +4. **Lista braków jest w `docs/SECURITY.md`, sekcja 8.** Najpilniejsze: mirror na GitHub + i 2FA na koncie OVH — panel OVH może maszynę zreinstalować, więc w praktyce + jest ważniejszy niż dostęp SSH. + +--- + +## 7. Czego nie robić + +- Nie startuj bootstrapu bez potwierdzenia — Kacper może chcieć najpierw przeczytać runbooki +- Nie zmieniaj `sshd_config` bez otwartej drugiej sesji SSH +- Nie zakładaj, że coś działa — sprawdzaj (`curl`, `docker ps`, logi) +- Nie rób `docker system prune -a --volumes` odruchowo +- Nie wracaj do rozstrzygniętych decyzji z sekcji 4 bez konkretnego powodu diff --git a/docs/SECURITY.md b/docs/SECURITY.md new file mode 100644 index 0000000..76f8118 --- /dev/null +++ b/docs/SECURITY.md @@ -0,0 +1,149 @@ +# SECURITY.md — bezpieczeństwo serwera + +Zasada porządkująca: **serwer jest tak bezpieczny, jak jego najsłabsza wystawiona usługa.** +Utwardzony SSH nie ma znaczenia, jeśli obok stoi panel administracyjny bez hasła. + +--- + +## 1. Warstwy + +| Warstwa | Czym chronimy | Gdzie skonfigurowane | +|---|---|---| +| Dostęp do maszyny | SSH tylko na klucz, root wyłączony | `/etc/ssh/sshd_config.d/99-hardening.conf` | +| Sieć | ufw: deny incoming, allow 22/80/443 | `ufw status verbose` | +| Automatyczne ataki | fail2ban na sshd | `/etc/fail2ban/jail.local` | +| Aktualizacje | unattended-upgrades (security) | `/etc/apt/apt.conf.d/50unattended-upgrades` | +| Izolacja aplikacji | osobne sieci Dockera, brak publikowanych portów | compose każdego stacka | +| Transport | TLS z Let's Encrypt na wszystkim | Traefik | +| Sekrety | pliki `.env`, `chmod 600`, poza repo | obok każdego stacka | + +--- + +## 2. SSH + +Docelowa konfiguracja: + +``` +PermitRootLogin no +PasswordAuthentication no +KbdInteractiveAuthentication no +PubkeyAuthentication yes +AllowUsers ubuntu +X11Forwarding no +MaxAuthTries 3 +``` + +**Reguła bezwzględna przy każdej zmianie w sshd:** otwierasz drugą sesję SSH i sprawdzasz, +że logowanie działa, **zanim** zamkniesz pierwszą. Błąd w `sshd_config` przy zamkniętej +sesji oznacza odzyskiwanie dostępu przez konsolę KVM w panelu OVH. + +Port zostawiamy na 22. Przenoszenie na 2222 to kosmetyka — odsiewa hałas w logach, +ale nie zatrzymuje nikogo, kto celuje w konkretną maszynę. fail2ban robi tu realną robotę. + +### Klucze + +- osobny klucz na każde urządzenie, `ed25519`, z komentarzem identyfikującym sprzęt +- klucz prywatny **nigdy** nie trafia na serwer ani do repo +- rejestr kluczy: `SERVER.md`, sekcja 4 +- przy zgubieniu urządzenia: usuwasz jego linię z `~/.ssh/authorized_keys`, koniec + +iOS: Termius lub Blink. Wchodzisz przez `tmux attach -t claude` do tej samej sesji, +którą zostawiłeś na Macu. + +--- + +## 3. Sekrety + +| Zasada | Dlaczego | +|---|---| +| `.env` obok stacka, `chmod 600`, właściciel `ubuntu` | inni użytkownicy systemu nie odczytają | +| `.env` w `.gitignore`, w repo tylko `.env.example` | sekret w historii gita zostaje tam na zawsze | +| Hasła generowane, nie wymyślane — `openssl rand -base64 32` | brak ponownego użycia | +| Osobny sekret na usługę | wyciek jednego nie kompromituje reszty | + +Jeśli sekret **przypadkiem** trafi do repo: nie wystarczy commit usuwający go. +Trzeba go **unieważnić i wygenerować nowy** — historia gita pamięta, a jeśli poszła na mirror +GitHuba, to jest już poza Twoją kontrolą. + +--- + +## 4. Usługi administracyjne + +Panel Traefika, bazy danych, narzędzia debugowe — **nie wystawiamy publicznie**. +Dostęp przez tunel SSH: + +```bash +ssh -L 8080:127.0.0.1:8080 ubuntu@dfkk.cloud +# potem localhost:8080 w przeglądarce +``` + +Jeśli coś musi być publiczne (np. `status.dfkk.cloud`, żeby działało z telefonu), +to za basic auth w Traefiku, z hasłem z generatora. + +**Gitea: rejestracja nowych użytkowników wyłączona natychmiast po instalacji.** +Otwarta rejestracja na publicznej instancji to zaproszenie dla botów, które +w kilka dni zamienią ją w hosting spamu. + +--- + +## 5. Aktualizacje + +| Co | Jak | Kiedy | +|---|---|---| +| Poprawki bezpieczeństwa Ubuntu | `unattended-upgrades`, automatycznie | codziennie | +| Reszta pakietów systemu | ręcznie, `apt upgrade` | miesięczny przegląd | +| Obrazy Dockera | ręcznie, podniesienie tagu wersji w compose | miesięczny przegląd | +| Restart po aktualizacji kernela | ręcznie, po snapshocie | gdy `/var/run/reboot-required` | + +Automatyczny restart po aktualizacji jest **wyłączony**. Nie chcemy, żeby maszyna +sama się przeładowała w środku pracy albo w trakcie działania czegoś produkcyjnego. + +Obrazy pinujemy do konkretnych wersji. `latest` oznacza, że najbliższy `docker compose pull` +może podmienić działającą aplikację na wersję z breaking changes — bez ostrzeżenia. + +--- + +## 6. Co monitorujemy + +| Sygnał | Próg | Reakcja | +|---|---|---| +| Zajętość dysku | 75% | sprzątanie wg `40-utrzymanie.md` | +| Usługa nie odpowiada | 2 nieudane sprawdzenia | alert mailem | +| Certyfikat wygasa | < 14 dni | sprawdzić DNS-01 i klucze OVH | +| Nietypowe wpisy w logach SSH | — | przegląd przy miesięcznym audycie | + +Alerty idą na `k.kruszka@hotmail.com` przez zewnętrzny SMTP. +Serwera pocztowego tu nie stawiamy — OVH blokuje port 25. + +--- + +## 7. Podejrzenie włamania + +Kolejność ma znaczenie. **Nie restartuj i nie „posprzątaj" najpierw** — zniszczysz ślady +i stracisz możliwość ustalenia, czym weszli. + +1. Odetnij ruch: `ufw default deny incoming` i zatrzymaj wystawione kontenery +2. **Zrób snapshot OVH** — to Twój materiał dowodowy +3. Zbierz: `last`, `journalctl -u ssh`, `docker ps -a`, `crontab -l` dla wszystkich użytkowników, + `ls -la /tmp /var/tmp`, nietypowe procesy i połączenia (`ss -tulpn`) +4. Unieważnij **wszystko**: klucze SSH, tokeny API OVH, token GitHub, hasła w `.env` +5. Oceń zakres — jeśli atakujący miał roota, maszyna jest spalona. + Postawienie od zera z repo `infra` + backup danych jest szybsze i pewniejsze + niż czyszczenie. Dlatego repo `infra` musi być kompletne. +6. Dopiero potem: odbudowa, i wpis do `SERVER.md` co się stało + +--- + +## 8. Braki, które trzeba domknąć + +Uczciwa lista rzeczy, których jeszcze **nie ma**. Przeglądać przy miesięcznym audycie. + +- [ ] **Backup off-site (restic).** Snapshot OVH to rollback, nie backup — jeden slot, + ręczny, nadpisywany, w tej samej infrastrukturze. Do domknięcia **zanim** + na serwerze pojawią się pierwsze prawdziwe dane. +- [ ] **Test odtworzenia z backupu.** Backup, którego nie odtworzyłeś, nie istnieje. +- [ ] **Mirror repo `infra` na GitHub.** Bez tego awaria serwera zabiera kod, historię + i konfigurację potrzebną do odtworzenia serwera — naraz. +- [ ] **2FA na koncie OVH.** Panel OVH może zrobić z maszyną wszystko, łącznie + z reinstalacją. Jest w praktyce ważniejszy niż dostęp SSH. +- [ ] **2FA na koncie GitHub.** diff --git a/docs/SERVER.md b/docs/SERVER.md new file mode 100644 index 0000000..1dd5084 --- /dev/null +++ b/docs/SERVER.md @@ -0,0 +1,131 @@ +# SERVER.md — rejestr serwera + +**Źródło prawdy o tym, co stoi na maszynie.** Każda zmiana stanu serwera = wpis tutaj, +w tej samej sesji. Jeśli czegoś nie ma w tym pliku — z punktu widzenia dokumentacji to nie istnieje. + +Ostatnia aktualizacja: _(uzupełnić przy pierwszym bootstrapie)_ + +--- + +## 1. Maszyna + +| Pozycja | Wartość | +|---|---| +| Dostawca | OVH | +| Model | VPS-2 2026 | +| Host OVH | `vps-c936b594.vps.ovh.net` | +| vCore / RAM / dysk | 6 / 12 GB / 100 GB SSD NVMe | +| System | Ubuntu 24.04 LTS | +| Lokalizacja | _(uzupełnić)_ | +| IPv4 | `54.38.159.156` | +| IPv6 | _(uzupełnić)_ | +| Snapshot OVH | włączony, 1 slot, ręczny | +| Data ostatniego snapshota | _(uzupełniać przy każdym)_ | + +--- + +## 2. Domeny + +| Domena | Rola | Rejestrator | DNS | +|---|---|---|---| +| `dfkk.cloud` | infrastruktura + subdomeny projektów | OVH | OVH | +| _(domena 2)_ | wolna, pod projekt | OVH | OVH | +| _(domena 3)_ | wolna, pod projekt | OVH | OVH | + +Certyfikaty: Let's Encrypt, wildcard `*.dfkk.cloud`, wyzwanie **DNS-01** przez API OVH. +Klucze API OVH: przechowywane w `/srv/infra/stacks/traefik/.env` (poza repo). + +### Zajęte subdomeny + +| Subdomena | Usługa | Publiczna? | Uwagi | +|---|---|---|---| +| `git.dfkk.cloud` | Gitea | tak | rejestracja wyłączona | +| `status.dfkk.cloud` | Uptime Kuma | tak | za basic auth | +| `traefik.dfkk.cloud` | Traefik dashboard | nie | tylko przez tunel SSH | + +--- + +## 3. Użytkownicy systemowi + +| Użytkownik | Rola | sudo | Uwagi | +|---|---|---|---| +| `root` | — | — | logowanie po SSH wyłączone przez OVH i przez nas | +| `ubuntu` | praca codzienna, Docker, Claude Code | tak | konto domyślne obrazu OVH | + +--- + +## 4. Klucze SSH + +Każde urządzenie ma **własny** klucz. Zgubiony laptop = usunięcie jednej linii +z `authorized_keys`, bez ruszania pozostałych. + +| Urządzenie | Typ | Komentarz w kluczu | Dodany | Status | +|---|---|---|---|---| +| MacBook | ed25519 | `kamil@macbook` | _(data)_ | _(aktywny)_ | +| Windows | ed25519 | `kamil@windows` | _(data)_ | _(aktywny)_ | +| iPhone (Termius/Blink) | ed25519 | `kamil@iphone` | _(data)_ | _(aktywny)_ | +| _(klucz istniejący, do weryfikacji)_ | ? | ? | ? | **do sprawdzenia przy bootstrapie** | + +--- + +## 5. Porty otwarte na świat + +| Port | Usługa | Uzasadnienie | +|---|---|---| +| 22 | SSH | dostęp administracyjny, tylko klucze | +| 80 | Traefik | przekierowanie na 443 | +| 443 | Traefik | cały ruch HTTPS | + +**Nic więcej.** Każdy dodatkowy port wymaga wpisu tutaj z uzasadnieniem. +Pamiętaj: opublikowany port kontenera omija ufw — patrz `CLAUDE.md`, sekcja 3.3. + +--- + +## 6. Stacki infrastruktury + +| Stack | Ścieżka | Status | Opis | +|---|---|---|---| +| Traefik | `/srv/infra/stacks/traefik` | _(planowany)_ | reverse proxy, TLS, wejście do wszystkiego | +| Gitea | `/srv/infra/stacks/gitea` | _(planowany)_ | repozytoria kodu, mirror na GitHub | +| Monitoring | `/srv/infra/stacks/monitoring` | _(planowany)_ | Uptime Kuma, alerty na maila | + +--- + +## 7. Projekty + +Tabela uzupełniana przy każdym nowym projekcie (runbook `10-nowa-usluga.md`) +i czyszczona przy usuwaniu (runbook `30-usuwanie.md`). + +| Projekt | Katalog | Repo | Domena | Wolumeny | Baza | Backup | Uruchomiony | +|---|---|---|---|---|---|---|---| +| _(brak)_ | | | | | | | | + +--- + +## 8. Zadania cykliczne (cron / systemd timers) + +| Zadanie | Harmonogram | Gdzie zdefiniowane | Co robi | +|---|---|---|---| +| `unattended-upgrades` | codziennie | systemd | aktualizacje bezpieczeństwa | +| _(uzupełniać)_ | | | | + +--- + +## 9. Konta zewnętrzne powiązane z serwerem + +| Usługa | Do czego | Gdzie klucz/token | +|---|---|---| +| OVH API | DNS-01 dla Traefika | `/srv/infra/stacks/traefik/.env` | +| GitHub | mirror repo `infra` | token w Gitei, ustawienia push mirror | +| SMTP (Resend/Brevo) | wysyłka maili z aplikacji | `.env` danego projektu | + +--- + +## 10. Dziennik większych zmian + +Krótko, jedna linia, najnowsze na górze. Chodzi o to, żeby po miesiącu dało się +odtworzyć, co się działo — nie o pełny changelog. + +| Data | Co się zmieniło | Kto | +|---|---|---| +| 2026-08-20 | Poprawki `00-bootstrap.md` (Krok 9 tmux append + `focus-events`, Krok 10 katalogi, Krok 12 Claude Code jako wykonany, nowy Krok 14 audyt) i numeracja `HANDOFF.md` sekcja 6 | Claude | diff --git a/docs/runbooks/00-bootstrap.md b/docs/runbooks/00-bootstrap.md new file mode 100644 index 0000000..ce1c0f7 --- /dev/null +++ b/docs/runbooks/00-bootstrap.md @@ -0,0 +1,392 @@ +# Runbook 00 — bootstrap serwera od zera + +Uruchomienie świeżego Ubuntu 24.04 do stanu, w którym można stawiać usługi. +Kolejność ma znaczenie — nie przeskakuj kroków. + +Czas: około 1–1,5 h przy pierwszym przejściu, z tłumaczeniem po drodze. + +--- + +## Krok 0 — przed startem + +- [ ] Znasz IPv4 serwera (`54.38.159.156`) i masz dostęp do panelu OVH +- [ ] Masz pod ręką **konsolę KVM w panelu OVH** — to droga ratunku, jeśli zablokujesz sobie SSH +- [ ] Wiesz, z których urządzeń będziesz się logował + +Obraz OVH nie daje logowania na roota po SSH. Konto robocze to **`ubuntu`**, z `sudo`. + +```bash +ssh ubuntu@54.38.159.156 +cat ~/.ssh/authorized_keys # jakie klucze już są +hostnamectl # wersja systemu +df -h / # punkt odniesienia dla dysku +sudo whoami # ma zwrócić: root +``` + +--- + +## Krok 1 — podstawy systemu + +```bash +sudo apt update && sudo apt upgrade -y +sudo timedatectl set-timezone Europe/Warsaw +sudo hostnamectl set-hostname dfkk +sudo apt install -y tmux curl git ufw fail2ban unattended-upgrades ca-certificates +echo "127.0.1.1 dfkk" | sudo tee -a /etc/hosts +``` + +--- + +## Krok 2 — klucze SSH ze WSZYSTKICH urządzeń + +**To jest krok, którego pominięcie kończy się utratą dostępu.** Krok 3 wyłącza +logowanie hasłem. Każde urządzenie, które nie ma wtedy klucza na serwerze, +traci możliwość zalogowania się — bo hasło przestaje działać, a klucza nie ma. + +Zrób to **dla każdego urządzenia osobno**: macOS, Windows, iOS. + +### Generowanie klucza (lokalnie, na danym urządzeniu) + +macOS / Linux: + +```bash +ssh-keygen -t ed25519 -C "kacper@macbook" +``` + +Windows (PowerShell): + +```powershell +ssh-keygen -t ed25519 -C "kacper@windows" +``` + +iOS: klucz generujesz w aplikacji (Termius lub Blink) i kopiujesz część publiczną. + +Komentarz na końcu (`-C`) jest istotny — po nim rozpoznasz w `authorized_keys`, +który wpis usunąć, gdy zgubisz konkretny sprzęt. + +### Wgranie klucza na serwer + +macOS / Linux: + +```bash +ssh-copy-id ubuntu@54.38.159.156 +``` + +Windows (PowerShell — nie ma `ssh-copy-id`): + +```powershell +type $env:USERPROFILE\.ssh\id_ed25519.pub | ssh ubuntu@54.38.159.156 "mkdir -p ~/.ssh && chmod 700 ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys" +``` + +### Weryfikacja — obowiązkowa + +Z **każdego** urządzenia po kolei: + +```bash +ssh ubuntu@54.38.159.156 +``` + +Ma wpuścić **bez pytania o hasło**. Jeśli pyta o hasło — klucz nie działa. +Napraw to teraz. Nie przechodź do kroku 3. + +Na serwerze sprawdź komplet: + +```bash +cat ~/.ssh/authorized_keys +``` + +Powinieneś zobaczyć klucz OVH plus jeden wpis na każde urządzenie, każdy z komentarzem. +Wpisz je do `docs/SERVER.md`, sekcja 4. + +### Hasło do sudo + +```bash +sudo passwd ubuntu +``` + +> **Decyzja do podjęcia:** `sudo` z hasłem czy `NOPASSWD`? +> Z hasłem jest bezpieczniej, ale każda operacja Claude Code wymagająca uprawnień +> zatrzyma się na monicie. `NOPASSWD` jest wygodniejszy i przy logowaniu wyłącznie +> na klucz — akceptowalny, bo kto ma klucz, i tak ma pełny dostęp. +> Rekomendacja: `NOPASSWD`, ale **hasło ustawione** jako furtka przez konsolę KVM. + +--- + +## Krok 3 — utwardzenie SSH + +```bash +sudo tee /etc/ssh/sshd_config.d/99-hardening.conf > /dev/null <<'EOF' +PermitRootLogin no +PasswordAuthentication no +KbdInteractiveAuthentication no +PubkeyAuthentication yes +AllowUsers ubuntu +X11Forwarding no +MaxAuthTries 3 +ClientAliveInterval 300 +ClientAliveCountMax 2 +EOF + +sudo sshd -t # walidacja składni — MUSI przejść bez błędu +sudo systemctl restart ssh +``` + +**Znowu: nowe okno, test logowania, i dopiero potem zamknięcie starej sesji.** +`sshd -t` łapie literówki, ale nie złapie sytuacji, w której wykluczyłeś sam siebie z `AllowUsers`. + +--- + +## Krok 4 — firewall + +```bash +sudo ufw default deny incoming +sudo ufw default allow outgoing +sudo ufw allow 22/tcp +sudo ufw allow 80/tcp +sudo ufw allow 443/tcp +sudo ufw enable +sudo ufw status verbose +``` + +> **Do zapamiętania:** ufw **nie chroni portów publikowanych przez Dockera.** +> Docker wpisuje własne reguły do iptables wcześniej w łańcuchu. Kontener z +> `ports: "5432:5432"` jest widoczny z internetu, mimo że ufw pokazuje `deny`. +> Dlatego w tym projekcie kontenery nie publikują portów — patrz `CLAUDE.md`, 3.3. + +--- + +## Krok 5 — fail2ban + +```bash +sudo tee /etc/fail2ban/jail.local > /dev/null <<'EOF' +[DEFAULT] +bantime = 1h +findtime = 10m +maxretry = 5 +backend = systemd + +[sshd] +enabled = true +EOF + +sudo systemctl enable --now fail2ban +sudo fail2ban-client status sshd +``` + +--- + +## Krok 6 — automatyczne aktualizacje bezpieczeństwa + +```bash +sudo dpkg-reconfigure -plow unattended-upgrades +``` + +Sprawdź, że `Unattended-Upgrade::Automatic-Reboot` jest ustawione na `"false"`. +Serwer ma się **nie restartować sam** — restart robimy świadomie, po snapshocie. + +--- + +## Krok 7 — swap + +12 GB RAM to dużo, ale swap chroni przed tym, że jeden kontener po wycieku pamięci +zabije OOM-killerem coś zupełnie innego. + +```bash +sudo fallocate -l 2G /swapfile +sudo chmod 600 /swapfile +sudo mkswap /swapfile +sudo swapon /swapfile +echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab +echo 'vm.swappiness=10' | sudo tee /etc/sysctl.d/99-swap.conf +sudo sysctl --system +``` + +--- + +## Krok 8 — Docker + +Z oficjalnego repozytorium Dockera, nie z repo Ubuntu (tam jest stara wersja). + +```bash +sudo install -m 0755 -d /etc/apt/keyrings +curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \ + sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg +sudo chmod a+r /etc/apt/keyrings/docker.gpg + +echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \ +https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" | \ + sudo tee /etc/apt/sources.list.d/docker.list > /dev/null + +sudo apt update +sudo apt install -y docker-ce docker-ce-cli containerd.io \ + docker-buildx-plugin docker-compose-plugin + +sudo usermod -aG docker ubuntu +``` + +Wyloguj się i zaloguj ponownie, żeby członkostwo w grupie `docker` zadziałało. + +### Limity logów — to jest ten krok, którego pominięcie zapycha dysk + +```bash +sudo tee /etc/docker/daemon.json > /dev/null <<'EOF' +{ + "log-driver": "json-file", + "log-opts": { + "max-size": "10m", + "max-file": "3" + }, + "live-restore": true +} +EOF + +sudo systemctl restart docker +docker run --rm hello-world +``` + +Bez tego log jednego gadatliwego kontenera rośnie w nieskończoność. Przy 100 GB +dysku to kwestia tygodni, nie lat. + +### Sieć `edge` + +```bash +docker network create edge +``` + +--- + +## Krok 9 — tmux + +Dopisujemy do `~/.tmux.conf`, nie nadpisujemy — jeśli plik już istnieje z własną +konfiguracją, `tee` bez `-a` go zniszczy. + +```bash +tee -a ~/.tmux.conf > /dev/null <<'EOF' +set -g mouse on +set -g history-limit 50000 +set -g base-index 1 +setw -g mode-keys vi +set -g status-bg colour236 +set -g status-fg colour252 +set -g focus-events on +EOF +tmux source-file ~/.tmux.conf # przeładowanie, jeśli tmux już działa +``` + +Od tego momentu praca zaczyna się od `tmux new -s bootstrap` albo `tmux attach -t bootstrap`. + +--- + +## Krok 10 — struktura katalogów + +`/srv` i `/srv/infra` już istnieją — repo `infra` zostało wgrane przez `scp` przed +bootstrapem. Brakuje `/srv/apps` i `/srv/data`. + +```bash +sudo mkdir -p /srv/apps /srv/data +sudo chown -R ubuntu:ubuntu /srv +``` + +--- + +## Krok 11 — snapshot + +**Zrób snapshot w panelu OVH teraz.** Masz czysty, utwardzony system bez usług — +to najlepszy punkt powrotu, jaki będziesz miał. Zapisz datę w `SERVER.md`. + +--- + +## Krok 12 — Claude Code + +**Wykonane 20-08-2026.** Instalacja natywnym instalatorem, bez Node.js: + +```bash +curl -fsSL https://claude.ai/install.sh | bash +``` + +Binarka ląduje w `~/.local/bin/claude`. Sesje odpalane **zawsze w tmuxie**, +z katalogu `/srv/infra`, żeby od razu widział `CLAUDE.md`. + +--- + +## Krok 13 — Traefik, Gitea, monitoring + +Osobne stacki, stawiane w tej kolejności: + +1. **Traefik** — bez niego reszta nie ma jak wyjść na świat. Wymaga kluczy API OVH + do DNS-01. Weryfikacja: certyfikat wildcard dla `*.dfkk.cloud` się wystawia. +2. **Gitea** na `git.dfkk.cloud` — **rejestracja wyłączona natychmiast po pierwszym + logowaniu**. Potem push mirror repo `infra` na prywatne repo GitHuba. +3. **Monitoring** (Uptime Kuma) na `status.dfkk.cloud` za basic auth, alerty na maila. + +Każdy stack: katalog w `/srv/infra/stacks/`, `compose.yaml` w repo, `.env` poza repo, +wpis w `SERVER.md`. + +--- + +## Krok 14 — audyt po bootstrapie + +Konfiguracja bez weryfikacji to założenie, nie zabezpieczenie. Literówka w +`sshd_config` albo reguła ufw, która nie weszła, wyglądają identycznie jak sukces. +**Każdy punkt, który nie przejdzie, blokuje przejście dalej.** + +### Na serwerze + +```bash +sudo sshd -T | grep -E "permitrootlogin|passwordauthentication|pubkeyauthentication" +# oczekiwane: no / no / yes + +sudo ufw status verbose +# tylko 22/80/443, default deny incoming + +sudo fail2ban-client status sshd +# jail aktywny + +systemctl is-enabled unattended-upgrades +# enabled + +grep Automatic-Reboot /etc/apt/apt.conf.d/50unattended-upgrades +# "false" + +sudo swapon --show +# swapfile 2G aktywny + +cat /etc/docker/daemon.json +# limity logów obecne + +docker ps --format "table {{.Names}}\t{{.Ports}}" +# nic na 0.0.0.0 + +ss -tulpn | grep LISTEN +# każdy nasłuch uzasadniony, porównaj z SERVER.md sekcja 5 + +cat ~/.ssh/authorized_keys +# tylko znane klucze, każdy z komentarzem identyfikującym urządzenie +``` + +### Do wykonania przez Kacpra, z jego urządzeń (Claude nie ma do nich dostępu) + +- `ssh ubuntu@54.38.159.156` z macOS, Windows i iOS → wchodzi bez pytania o hasło +- test, że baza nie jest wystawiona — na Windowsie w PowerShellu: + + ```powershell + Test-NetConnection -ComputerName 54.38.159.156 -Port 5432 + # oczekiwane: TcpTestSucceeded : False + ``` + + (Windows nie ma netcata — nie używamy tu `nc -zv`.) + +### Po audycie + +Zaktualizować `SERVER.md` sekcje 4, 5 i 10, zrobić commit. + +--- + +## Po zakończeniu + +- [ ] `SERVER.md` uzupełniony: IP, lokalizacja, klucze, data snapshota +- [ ] Repo `infra` zainicjowane i zacommitowane +- [ ] Mirror na GitHub skonfigurowany +- [ ] Test: rozłączenie SSH w trakcie działania procesu w tmux — proces przeżywa +- [ ] Test: logowanie z każdego z trzech urządzeń +- [ ] Snapshot po postawieniu Traefika i Gitei diff --git a/docs/runbooks/10-nowa-usluga.md b/docs/runbooks/10-nowa-usluga.md new file mode 100644 index 0000000..9cb0008 --- /dev/null +++ b/docs/runbooks/10-nowa-usluga.md @@ -0,0 +1,138 @@ +# Runbook 10 — nowa usługa + +Od pustego katalogu do działającej aplikacji na własnej subdomenie z HTTPS. + +--- + +## 1. Ustalenia przed startem + +| Pytanie | Po co | +|---|---| +| Nazwa projektu (małe litery, myślnik) | katalog, kontenery, sieć, subdomena | +| Publiczna czy wewnętrzna? | czy w ogóle wchodzi do sieci `edge` | +| Baza danych? Jaka? | osobny kontener w sieci wewnętrznej + wpis do backupu | +| Dane trwałe? Gdzie? | bind-mount w `/srv/data/` | +| Wysyłka maili? | zewnętrzny SMTP, nigdy lokalny | +| Szacowane zużycie dysku | 100 GB to nasz limit | + +--- + +## 2. Struktura + +```bash +mkdir -p /srv/apps/ +mkdir -p /srv/data/ +cd /srv/apps/ +git init +``` + +Minimum plików: `compose.yaml`, `.env.example`, `.gitignore` (z `.env`), `README.md`. + +--- + +## 3. Wzorzec compose + +```yaml +services: + web: + image: : # nigdy :latest + container_name: -web + restart: unless-stopped + env_file: .env + networks: + - edge + - internal + volumes: + - /srv/data//web:/app/data + labels: + - "traefik.enable=true" + - "traefik.http.routers..rule=Host(`.dfkk.cloud`)" + - "traefik.http.routers..entrypoints=websecure" + - "traefik.http.routers..tls.certresolver=ovh" + - "traefik.http.services..loadbalancer.server.port=" + + db: + image: postgres:16.4 + container_name: -db + restart: unless-stopped + env_file: .env + networks: + - internal # BEZ edge — baza nie widzi świata + volumes: + - /srv/data//db:/var/lib/postgresql/data + +networks: + edge: + external: true + internal: + name: -internal +``` + +**Trzy rzeczy, na które patrzysz w tym pliku:** + +1. Brak sekcji `ports:` — ruch wchodzi wyłącznie przez Traefika. + Opublikowany port omija ufw i jest widoczny z internetu. +2. `db` jest tylko w `internal`. Kontener w sieci `edge` jest osiągalny + dla każdej innej usługi w `edge` — baza nie ma tam czego szukać. +3. Wersja obrazu przypięta. `:latest` oznacza, że przyszły `pull` może + podmienić działającą aplikację na wersję z breaking changes. + +--- + +## 4. Sekrety + +```bash +cp .env.example .env +chmod 600 .env +openssl rand -base64 32 # tak generujemy hasła, nie wymyślamy ich +``` + +Sprawdź, że `.env` jest w `.gitignore`, **zanim** zrobisz pierwszy commit. +Sekret raz wypchnięty do repo trzeba unieważnić, nie usunąć. + +--- + +## 5. DNS + +Rekord A dla `.dfkk.cloud` na IP serwera — panel OVH. +Certyfikat leci z wildcardu `*.dfkk.cloud`, więc **nic dodatkowo nie robisz**. +Szczegóły: `20-domena.md`. + +--- + +## 6. Uruchomienie + +```bash +docker compose config # walidacja składni i podstawionych zmiennych +docker compose up -d +docker compose ps +docker compose logs -f --tail=50 +``` + +--- + +## 7. Weryfikacja — sprawdzasz, nie zakładasz + +```bash +curl -I https://.dfkk.cloud # 200 lub 3xx, certyfikat ważny +docker compose ps # wszystko "Up", nic w restart loop +docker stats --no-stream # zużycie w granicach rozsądku +``` + +Test negatywny — potwierdzenie, że baza **nie** jest wystawiona: + +```bash +# z lokalnej maszyny, nie z serwera: +nc -zv 5432 # ma odmówić połączenia +``` + +--- + +## 8. Domknięcie + +- [ ] Commit w repo projektu, push do Gitei +- [ ] Wpis w `SERVER.md`: projekt, katalog, repo, domena, wolumeny, baza +- [ ] Wpis w `SERVER.md`, sekcja 2: zajęta subdomena +- [ ] Jeśli są dane trwałe — dopisanie ścieżki do listy backupu +- [ ] Monitor w Uptime Kuma +- [ ] `df -h` — kontrola, ile dysku ubyło diff --git a/docs/runbooks/20-domena.md b/docs/runbooks/20-domena.md new file mode 100644 index 0000000..9c78f70 --- /dev/null +++ b/docs/runbooks/20-domena.md @@ -0,0 +1,101 @@ +# Runbook 20 — domeny i DNS + +Wszystkie trzy domeny są w OVH, DNS też w OVH. To upraszcza sprawę: Traefik może +używać API OVH do wyzwania **DNS-01**, więc dostajemy certyfikaty wildcard. + +--- + +## 1. Dlaczego DNS-01, a nie HTTP-01 + +| | HTTP-01 | DNS-01 (nasz wybór) | +|---|---|---| +| Jak działa | Let's Encrypt puka na port 80 | Traefik wpisuje rekord TXT przez API | +| Wildcard | niemożliwy | możliwy | +| Nowa subdomena | osobny certyfikat, osobne wyzwanie | działa od razu, zero akcji | +| Usługi niepubliczne | nie da się | da się | +| Wymaga | otwartego portu 80 | kluczy API dostawcy DNS | + +Przy wildcardzie `*.dfkk.cloud` dodanie `nowyprojekt.dfkk.cloud` to jeden rekord A +i restart kontenera. Certyfikatem nie zajmujesz się w ogóle. + +--- + +## 2. Klucze API OVH — jednorazowo + +1. Wejdź na stronę tworzenia tokenów API OVH dla właściwego regionu konta + (uwaga: konto jest w regionie **CA** — patrz adres panelu w `SERVER.md`, + to zmienia zarówno adres, pod którym generujesz token, jak i endpoint w konfiguracji) +2. Uprawnienia zawężone do stref DNS — `GET`, `POST`, `PUT`, `DELETE` + na `/domain/zone/*`. Nie dawaj tokenowi pełnego dostępu do konta. +3. Ważność: bez limitu, ale **wpisz token do `SERVER.md`, sekcja 9**, żeby wiadomo było, + co unieważnić przy incydencie +4. Trzy wartości — application key, application secret, consumer key — + lądują w `/srv/infra/stacks/traefik/.env`, `chmod 600`, poza repo + +--- + +## 3. Nowa subdomena + +``` +Typ: A +Nazwa: +Cel: +TTL: 300 (na czas testów; potem można podnieść) +``` + +Sprawdzenie propagacji: + +```bash +dig +short .dfkk.cloud +``` + +Dopóki `dig` nie zwraca IP serwera, nie ma sensu debugować Traefika — +problem jest w DNS, nie w kontenerze. + +--- + +## 4. Nowa domena (druga lub trzecia) + +Wildcard obejmuje **tylko** `*.dfkk.cloud`. Kolejna domena wymaga: + +1. Rekordu A na IP serwera +2. Dopisania nowej domeny do konfiguracji certresolvera w Traefiku + (osobny wpis `domains` z `main` i `sans`) +3. Sprawdzenia, że token API OVH ma uprawnienia **także do tej strefy** +4. Wpisu w `SERVER.md`, sekcja 2 + +--- + +## 5. Odpinanie domeny + +Kolejność odwrotna niż przy podpinaniu: + +- [ ] Usuń labele Traefika z compose i zrestartuj usługę +- [ ] Usuń rekord A w panelu OVH +- [ ] Sprawdź, że nie ma innych rekordów wskazujących na serwer (CNAME, MX) +- [ ] Wykreśl subdomenę z `SERVER.md`, sekcja 2 +- [ ] Usuń monitor z Uptime Kuma + +Martwy rekord A wskazujący na Twój serwer to nie tylko bałagan — jeśli kiedyś oddasz +to IP, ktoś inny odziedziczy ruch kierowany na Twoją nazwę. + +--- + +## 6. Diagnostyka + +| Objaw | Gdzie szukać | +|---|---| +| `dig` nie zwraca IP | DNS, panel OVH — nie ruszaj serwera | +| DNS ok, ale 404 z Traefika | literówka w `Host()` albo brak `traefik.enable=true` | +| DNS ok, ale połączenie odrzucone | kontener nie działa lub nie jest w sieci `edge` | +| Błąd certyfikatu | logi Traefika, klucze API OVH, uprawnienia do strefy | +| Certyfikat „fake" / staging | w konfiguracji został adres staging Let's Encrypt | + +```bash +docker logs traefik --tail=100 | grep -i -E "acme|error|certificate" +``` + +> **Uwaga na limity Let's Encrypt.** Przy debugowaniu certyfikatów łatwo wpaść +> w limit żądań i zostać zablokowanym na tydzień. Jeśli certyfikat nie chce się +> wystawić — przełącz się na serwer **staging** Let's Encrypt, dopracuj konfigurację +> tam, i dopiero potem wróć na produkcyjny. diff --git a/docs/runbooks/30-usuwanie.md b/docs/runbooks/30-usuwanie.md new file mode 100644 index 0000000..ea36a67 --- /dev/null +++ b/docs/runbooks/30-usuwanie.md @@ -0,0 +1,138 @@ +# Runbook 30 — usuwanie projektu + +Najważniejszy runbook dla utrzymania porządku. Usunięcie projektu to **nie** jest +`docker compose down`. Osierocone wolumeny, martwe rekordy DNS i wyłączone kontenery +zajmujące gigabajty to dokładnie ten bałagan, którego ten serwer ma nie mieć. + +**Przechodzisz całą listę. Za każdym razem.** + +--- + +## Krok 0 — zabezpieczenie danych + +Zanim cokolwiek skasujesz: + +```bash +cd /srv/apps/ +docker compose ps # co faktycznie działa +docker compose config # jakie wolumeny są w grze +du -sh /srv/data/ # ile tam danych +``` + +**Baza danych — zrzut przed usunięciem, zawsze:** + +```bash +docker compose exec db pg_dump -U > ~/-final-$(date +%F).sql +``` + +Trzymaj zrzut poza `/srv/data/` — inaczej skasujesz go razem z resztą. +Rozważ przeniesienie na lokalną maszynę. „Na pewno już niepotrzebne" bywa +weryfikowane dwa miesiące później. + +> **Punkt zatrzymania:** potwierdź z Kacprem, że dane są zbędne albo zabezpieczone. +> To operacja nieodwracalna. + +--- + +## Krok 1 — zatrzymanie + +```bash +docker compose down # BEZ -v na tym etapie +docker compose ps -a +``` + +`-v` kasuje wolumeny natychmiast. Zostawiamy je do kroku 4, żeby był margines na refleksję. + +--- + +## Krok 2 — DNS i Traefik + +- [ ] Usuń rekord A w panelu OVH +- [ ] `dig +short .dfkk.cloud` — ma nie zwracać nic +- [ ] Sprawdź, że żadna inna usługa nie używa tej subdomeny + +--- + +## Krok 3 — monitoring + +- [ ] Usuń monitor z Uptime Kuma (inaczej dostaniesz alert o „awarii" usługi, + którą sam skasowałeś) +- [ ] Usuń ścieżki projektu z listy backupu + +--- + +## Krok 4 — dane i wolumeny + +```bash +docker volume ls | grep +docker volume rm # świadomie, po jednym +sudo rm -rf /srv/data/ +``` + +> **Punkt zatrzymania:** `rm -rf` na katalogu z danymi wymaga wyraźnej zgody. +> Przeczytaj ścieżkę na głos przed naciśnięciem Enter. + +--- + +## Krok 5 — obrazy i sieci + +```bash +docker images | grep +docker rmi # tylko jeśli nic innego go nie używa +docker network rm -internal +``` + +--- + +## Krok 6 — kod + +```bash +sudo rm -rf /srv/apps/ +``` + +Repo w Gitei: **archiwizuj, nie kasuj.** Archiwum jest tanie, kod czasem wraca. +Kasowanie repozytorium ma sens tylko wtedy, gdy trafiły do niego sekrety — +a wtedy i tak trzeba je najpierw unieważnić. + +--- + +## Krok 7 — dokumentacja + +- [ ] Wykreśl projekt z `SERVER.md`, sekcja 7 +- [ ] Wykreśl subdomenę z `SERVER.md`, sekcja 2 +- [ ] Usuń zadania cykliczne z sekcji 8, jeśli jakieś były +- [ ] Wpis w dzienniku zmian, sekcja 10 +- [ ] Commit w repo `infra` + +--- + +## Krok 8 — kontrola + +```bash +df -h / # miejsce faktycznie wróciło? +docker system df # ile zajmują obrazy, kontenery, wolumeny +docker ps -a # brak śladów po projekcie +docker volume ls -f dangling=true +``` + +Jeśli miejsce **nie** wróciło — coś zostało. Szukaj, zanim uznasz sprawę za zamkniętą. + +--- + +## Sprzątanie osieroconych zasobów + +`docker system prune` bywa kuszący. Zanim go użyjesz — sprawdź, co usunie: + +```bash +docker volume ls -f dangling=true # wolumeny bez kontenera +docker images -f dangling=true # warstwy bez tagu +``` + +**`docker system prune -a --volumes` potrafi skasować wolumen zatrzymanej usługi, +która wcale nie miała zniknąć.** Kasujemy pojedynczo, po sprawdzeniu każdej pozycji. +Bezpieczna wersja, bez dotykania wolumenów: + +```bash +docker image prune -a # tylko nieużywane obrazy +docker builder prune # cache budowania — zwykle największy zysk +``` diff --git a/docs/runbooks/40-utrzymanie.md b/docs/runbooks/40-utrzymanie.md new file mode 100644 index 0000000..52cb8c3 --- /dev/null +++ b/docs/runbooks/40-utrzymanie.md @@ -0,0 +1,121 @@ +# Runbook 40 — utrzymanie + +Serwer psuje się powoli i po cichu. Te przeglądy zajmują kilka minut i zapobiegają +sytuacji, w której o problemie dowiadujesz się od użytkownika. + +--- + +## Przegląd tygodniowy — 5 minut + +```bash +df -h / # < 75% +docker system df # co rośnie +docker ps -a # nic w restart loop +free -h # swap w użyciu? coś przecieka +uptime # load average +sudo fail2ban-client status sshd +``` + +Czerwone flagi: + +| Objaw | Co znaczy | +|---|---| +| Dysk > 75% | czas na sprzątanie, nie za tydzień | +| Kontener z rosnącym `RESTARTS` | crash loop — sprawdź logi | +| Swap mocno zajęty przy 12 GB RAM | wyciek pamięci w którymś kontenerze | +| Skok liczby banów fail2ban | ktoś się interesuje maszyną | + +--- + +## Przegląd miesięczny — 30 minut + +### Aktualizacje systemu + +```bash +sudo apt update +apt list --upgradable +``` + +Zanim `upgrade`: **snapshot OVH.** Potem: + +```bash +sudo apt upgrade -y +ls /var/run/reboot-required 2>/dev/null && echo "wymagany restart" +``` + +Restart robimy świadomie, po snapshocie, nie w środku pracy. + +### Aktualizacje obrazów + +Obrazy są przypięte do wersji, więc `pull` sam nic nie podniesie — i o to chodzi. +Podnoszenie wersji to świadoma decyzja: + +1. Sprawdź changelog obrazu pod kątem breaking changes +2. Podnieś tag w `compose.yaml` +3. Snapshot +4. `docker compose pull && docker compose up -d` +5. Weryfikacja: `curl -I`, logi, `docker compose ps` +6. Commit + +Jedna usługa naraz. Aktualizacja pięciu rzeczy jednocześnie oznacza, że przy awarii +nie wiesz, która zawiniła. + +### Sprzątanie dysku + +```bash +docker system df +docker image prune -a # bezpieczne +docker builder prune # zwykle najwięcej odzyskuje +sudo journalctl --disk-usage +sudo journalctl --vacuum-time=30d +``` + +Wolumeny — **nigdy hurtem**: + +```bash +docker volume ls -f dangling=true +# sprawdź każdy, potem docker volume rm +``` + +### Bezpieczeństwo + +```bash +sudo journalctl -u ssh --since "1 month ago" | grep -i "accepted" | tail -30 +sudo cat /home/ubuntu/.ssh/authorized_keys # tylko znane klucze? +sudo ufw status verbose # tylko 22/80/443? +docker ps --format "table {{.Names}}\t{{.Ports}}" # nic na 0.0.0.0? +``` + +Ostatnia komenda jest ważniejsza, niż wygląda. Opublikowany port kontenera na +`0.0.0.0` omija ufw i jest widoczny z internetu — a `ufw status` tego nie pokaże. + +### Certyfikaty + +```bash +echo | openssl s_client -connect git.dfkk.cloud:443 2>/dev/null | \ + openssl x509 -noout -dates +``` + +Odnowienie następuje automatycznie ~30 dni przed wygaśnięciem. Jeśli zostało +mniej niż 14 dni — coś się zacięło, sprawdź logi Traefika i klucze API OVH. + +### Dokumentacja + +- [ ] `SERVER.md` zgodny ze stanem faktycznym? +- [ ] Lista braków w `SECURITY.md`, sekcja 8 — coś do odhaczenia? +- [ ] Repo `infra` zacommitowane i zmirrorowane na GitHub? + +--- + +## Przegląd kwartalny + +- [ ] **Test odtworzenia z backupu.** Nie „sprawdzenie, że plik istnieje" — + faktyczne przywrócenie danych i weryfikacja, że są kompletne. + Backup, którego nie odtworzyłeś, to założenie, nie zabezpieczenie. +- [ ] Rotacja tokenów API (OVH, GitHub, SMTP) +- [ ] Przegląd kluczy SSH — czy wszystkie urządzenia z listy nadal istnieją i są Twoje +- [ ] Przegląd projektów: czy wszystko, co działa, jest jeszcze potrzebne? + Nieużywana usługa to zajęty dysk, otwarta powierzchnia ataku i pakiety, + których nikt nie aktualizuje. +- [ ] Weryfikacja, czy `CLAUDE.md` odpowiada temu, jak faktycznie pracujemy — + jeśli zasada jest regularnie omijana, trzeba ją zmienić albo zacząć stosować diff --git a/docs/runbooks/50-awaria.md b/docs/runbooks/50-awaria.md new file mode 100644 index 0000000..63f0d70 --- /dev/null +++ b/docs/runbooks/50-awaria.md @@ -0,0 +1,145 @@ +# Runbook 50 — awaria i rollback + +Do czytania, gdy coś nie działa. Zasada nadrzędna: **najpierw diagnoza, potem zmiany.** +Chaotyczne restartowanie wszystkiego zaciera ślady i zamienia jeden problem w trzy. + +--- + +## Kolejność diagnozy + +Idź od zewnątrz do środka. Zatrzymaj się na pierwszym kroku, który zawiedzie — +tam jest problem, dalej nie ma sensu szukać. + +```bash +# 1. Czy maszyna żyje? +ping +ssh ubuntu@ + +# 2. Czy nie brakuje zasobów? (najczęstsza przyczyna) +df -h / +free -h +uptime + +# 3. Czy Docker działa? +sudo systemctl status docker +docker ps + +# 4. Czy Traefik żyje? +docker logs traefik --tail=50 + +# 5. Czy konkretna usługa żyje? +cd /srv/apps/ +docker compose ps +docker compose logs --tail=100 +``` + +--- + +## Typowe przyczyny, w kolejności prawdopodobieństwa + +| Objaw | Sprawdź najpierw | +|---|---| +| Wszystko przestało działać naraz | `df -h` — pełny dysk zatrzymuje wszystko | +| Jedna usługa w restart loop | `docker compose logs` — zwykle brak zmiennej w `.env` lub zajęty port | +| 502 z Traefika | kontener działa, ale zły port w labelu `loadbalancer.server.port` | +| 404 z Traefika | zła reguła `Host()` albo brak `traefik.enable=true` | +| Błąd certyfikatu | logi Traefika, klucze API OVH, limity Let's Encrypt | +| Usługa wolna, serwer obciążony | `docker stats` — który kontener zjada CPU/RAM | +| Kontener zabity bez śladu | OOM killer: `dmesg -T \| grep -i oom` | + +--- + +## Pełny dysk — najczęstsza awaria tej maszyny + +Przy 100 GB to realne ryzyko. Objawy bywają mylące: bazy przestają zapisywać, +Docker nie startuje kontenerów, logi się urywają. + +```bash +df -h / +sudo du -h --max-depth=1 /var | sort -hr | head +sudo du -h --max-depth=1 /srv | sort -hr | head +docker system df +``` + +Szybkie odzyskanie miejsca, od najbezpieczniejszego: + +```bash +sudo journalctl --vacuum-size=200M +docker builder prune -f +docker image prune -a -f +``` + +Dopiero potem szukaj przyczyny — zwykle jest to jeden kontener logujący bez limitu +albo rosnąca baza. Limity logów są w `/etc/docker/daemon.json`. + +--- + +## Rollback + +### Poziom 1 — konfiguracja usługi + +```bash +cd /srv/apps/ +git log --oneline -10 +git revert +docker compose up -d +``` + +### Poziom 2 — wersja obrazu + +Cofnij tag w `compose.yaml` do poprzedniej działającej wersji, `docker compose up -d`. +Dlatego przypinamy wersje — z `:latest` nie masz do czego wrócić. + +### Poziom 3 — snapshot OVH + +Przywrócenie snapshota **cofa całą maszynę** do stanu z chwili jego zrobienia. +Wszystko, co powstało później — inne projekty, dane, commity — znika. + +Zanim to zrobisz: + +- [ ] Czy problem faktycznie wymaga cofnięcia całej maszyny? +- [ ] Co powstało od czasu snapshota i czego nieodwracalnie stracisz? +- [ ] Czy dasz radę wyciągnąć potrzebne dane przed przywróceniem? +- [ ] Data snapshota — jest w `SERVER.md` + +To ostateczność. W większości przypadków szybciej jest naprawić usługę. + +--- + +## Odzyskanie dostępu po zablokowaniu SSH + +Jeśli błąd w `sshd_config` albo ufw odciął Cię od maszyny: + +1. Panel OVH → Twój VPS → **konsola KVM** +2. Logowanie jako `root` lub `ubuntu` (dlatego `ubuntu` ma ustawione hasło — + przez konsolę klucz SSH nie pomoże) +3. Napraw konfigurację, `sudo sshd -t`, restart usługi +4. Test z zewnątrz **przed** zamknięciem konsoli + +To jest powód, dla którego przy każdej zmianie w SSH trzymamy otwartą drugą sesję. + +--- + +## Utrata Gitei + +Gitea stoi na tym samym serwerze, którym zarządza — więc jej awaria zabiera kod +i konfigurację naraz. Dlatego: + +- repo `infra` jest zmirrorowane na prywatne repo GitHuba +- na maszynie zawsze jest aktualna kopia robocza `/srv/infra` +- odtworzenie: sklonuj `infra` z GitHuba, postaw Traefika i Gitea z `00-bootstrap.md`, + krok 13, przywróć dane repozytoriów z backupu + +Jeśli mirror na GitHub nie jest jeszcze skonfigurowany — to najpilniejsza rzecz +z listy braków w `SECURITY.md`. + +--- + +## Po każdej awarii + +- [ ] Wpis w dzienniku zmian w `SERVER.md`: co się stało, co pomogło +- [ ] Jeśli przyczyna była systemowa — dopisz punkt kontrolny do `40-utrzymanie.md` +- [ ] Jeśli dało się temu zapobiec regułą — dopisz ją do `CLAUDE.md` + +Runbooki mają rosnąć razem z doświadczeniem. Awaria, z której nic nie wynikło +dla dokumentacji, powtórzy się.