commit f5899ebc5654c616b62077f38023dd82eb8773df Author: PTah Date: Tue Apr 21 21:37:43 2026 +1000 Initial own-git docs, configs, scripts, and hardening pack diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..17d446b --- /dev/null +++ b/.env.example @@ -0,0 +1,21 @@ +# GitHub source +GITHUB_TOKEN=ghp_xxx +GITHUB_OWNER=your_github_user_or_org +GITHUB_OWNER_TYPE=user + +# Gitea target +GITEA_URL=https://git.papatramp.ru +GITEA_TOKEN=gitea_pat_xxx +GITEA_OWNER=your_gitea_user_or_org +GITEA_OWNER_TYPE=user + +# Push strategy +PUSH_WITH_SSH=true +GITEA_SSH_HOST=git.papatramp.ru +GITEA_SSH_PORT=2222 +# Required only for HTTPS push mode +GITEA_USERNAME=your_gitea_username + +# Optional behavior +MIGRATE_LFS=true +WORKDIR=./migration-workdir diff --git a/.github/mlc_config.json b/.github/mlc_config.json new file mode 100644 index 0000000..41d4ea0 --- /dev/null +++ b/.github/mlc_config.json @@ -0,0 +1,16 @@ +{ + "ignorePatterns": [ + { + "pattern": "^ssh://" + }, + { + "pattern": "^git@" + } + ], + "replacementPatterns": [], + "timeout": "20s", + "retryOn429": true, + "retryCount": 2, + "fallbackRetryDelay": "2s", + "aliveStatusCodes": [200, 206, 301, 302, 303, 307, 308] +} diff --git a/.github/workflows/haproxy-lint.yml b/.github/workflows/haproxy-lint.yml new file mode 100644 index 0000000..3b75ae3 --- /dev/null +++ b/.github/workflows/haproxy-lint.yml @@ -0,0 +1,42 @@ +name: HAProxy Config Check + +on: + pull_request: + paths: + - "haproxy/**" + - ".github/workflows/haproxy-lint.yml" + push: + branches: + - main + paths: + - "haproxy/**" + - ".github/workflows/haproxy-lint.yml" + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install haproxy + run: | + sudo apt-get update + sudo apt-get install -y haproxy + + - name: Prepare include files and dummy cert + run: | + sudo mkdir -p /etc/haproxy /etc/haproxy/certs + sudo cp haproxy/haproxy.cfg /etc/haproxy/haproxy.cfg + echo "127.0.0.1/32" | sudo tee /etc/haproxy/allowed_ips.lst >/dev/null + echo "127.0.0.1/32" | sudo tee /etc/haproxy/allowed_ips_knock.lst >/dev/null + openssl req -x509 -nodes -newkey rsa:2048 \ + -keyout /tmp/tls.key \ + -out /tmp/tls.crt \ + -days 1 \ + -subj "/CN=git.papatramp.ru" + cat /tmp/tls.crt /tmp/tls.key | sudo tee /etc/haproxy/certs/papatramp.pem >/dev/null + + - name: Validate haproxy cfg + run: | + sudo haproxy -c -f /etc/haproxy/haproxy.cfg diff --git a/.github/workflows/scripts-and-docs-check.yml b/.github/workflows/scripts-and-docs-check.yml new file mode 100644 index 0000000..d3c6c2f --- /dev/null +++ b/.github/workflows/scripts-and-docs-check.yml @@ -0,0 +1,48 @@ +name: Scripts And Docs Check + +on: + pull_request: + paths: + - "scripts/**" + - "README.md" + - "docs/**" + - ".github/workflows/scripts-and-docs-check.yml" + push: + branches: + - main + paths: + - "scripts/**" + - "README.md" + - "docs/**" + - ".github/workflows/scripts-and-docs-check.yml" + +jobs: + shellcheck: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install shellcheck + run: | + sudo apt-get update + sudo apt-get install -y shellcheck + + - name: Run shellcheck + run: | + shellcheck scripts/*.sh + + markdown-links: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Check markdown links + uses: gaurav-nelson/github-action-markdown-link-check@v1 + with: + use-quiet-mode: "yes" + use-verbose-mode: "no" + config-file: ".github/mlc_config.json" + folder-path: "." + file-extension: ".md" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7d84f47 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +migration-workdir/ +*.log +*.tmp diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ee2b1ff --- /dev/null +++ b/Makefile @@ -0,0 +1,41 @@ +SHELL := /usr/bin/env bash + +.PHONY: help check-haproxy deploy-haproxy gitea-up gitea-restart migrate dry-run + +help: + @echo "Targets:" + @echo " check-haproxy - validate local haproxy/haproxy.cfg" + @echo " deploy-haproxy - copy cfg to /etc/haproxy and reload service" + @echo " gitea-up - start Gitea via docker compose" + @echo " gitea-restart - restart Gitea container" + @echo " migrate - run GitHub -> Gitea migration" + @echo " dry-run - show variables loaded from .env (safe preview)" + +check-haproxy: + sudo haproxy -c -f haproxy/haproxy.cfg + +deploy-haproxy: check-haproxy + sudo cp haproxy/haproxy.cfg /etc/haproxy/haproxy.cfg + sudo haproxy -c -f /etc/haproxy/haproxy.cfg + sudo systemctl reload haproxy + +gitea-up: + sudo mkdir -p /opt/gitea/data/gitea/conf + sudo cp gitea/docker-compose.yml /opt/gitea/docker-compose.yml + cd /opt/gitea && sudo docker compose up -d + +gitea-restart: + cd /opt/gitea && sudo docker compose restart gitea + +migrate: + @if [ ! -f .env ]; then echo ".env not found. Copy .env.example -> .env first"; exit 1; fi + set -a && source .env && set +a && ./scripts/migrate-github-to-gitea.sh + +dry-run: + @if [ ! -f .env ]; then echo ".env not found. Copy .env.example -> .env first"; exit 1; fi + set -a && source .env && set +a && \ + echo "GITHUB_OWNER=$$GITHUB_OWNER ($$GITHUB_OWNER_TYPE)" && \ + echo "GITEA_URL=$$GITEA_URL" && \ + echo "GITEA_OWNER=$$GITEA_OWNER ($$GITEA_OWNER_TYPE)" && \ + echo "PUSH_WITH_SSH=$$PUSH_WITH_SSH, GITEA_SSH_HOST=$$GITEA_SSH_HOST, GITEA_SSH_PORT=$$GITEA_SSH_PORT" && \ + echo "MIGRATE_LFS=$$MIGRATE_LFS" diff --git a/README.md b/README.md new file mode 100644 index 0000000..81219b2 --- /dev/null +++ b/README.md @@ -0,0 +1,276 @@ +# OwnGit: свой "аналог GitHub" на Ubuntu 24.04 + +Готовый комплект для поднятия собственного git-сервера (Gitea/Forgejo) за HAProxy: + +- reverse proxy и TLS на HAProxy; +- Gitea внутри сети; +- SSH для `git push/pull`; +- миграция репозиториев с GitHub (public + private). + +## Состав репозитория + +- `haproxy/haproxy.cfg` — полный конфиг HAProxy (с Gitea, Zabbix, Synology, RDP, SMTP). +- `gitea/docker-compose.yml` — запуск Gitea в Docker. +- `gitea/app.ini.example` — шаблон конфигурации Gitea. +- `scripts/migrate-github-to-gitea.sh` — массовая миграция репозиториев GitHub в Gitea. +- `scripts/preflight-check.sh` — быстрая проверка DNS/портов/healthz/SSH. +- `scripts/backup-gitea.sh` — бэкап данных Gitea с ротацией. +- `scripts/restore-gitea.sh` — восстановление Gitea из архива бэкапа. +- `scripts/setup-ufw.sh` — базовая настройка UFW под эту схему портов. +- `scripts/safe-apply-ssh-hardening.sh` — безопасное применение SSH hardening. +- `scripts/safe-apply-ssh-lockdown.sh` — безопасное применение строгого SSH lockdown-профиля. +- `scripts/bootstrap-own-git.sh` — one-shot установка стека (deps + Gitea + HAProxy + fail2ban + timer). +- `.env.example` — шаблон переменных окружения для миграции. +- `Makefile` — быстрые команды установки/проверки/миграции. +- `docs/troubleshooting.md` — типовые проблемы и диагностика. +- `docs/security-hardening.md` — чеклист усиления безопасности (P0/P1/P2). +- `.github/workflows/haproxy-lint.yml` — CI-проверка синтаксиса `haproxy.cfg`. +- `.github/workflows/scripts-and-docs-check.yml` — `shellcheck` и проверка markdown-ссылок. +- `systemd/` — unit/timer для ежедневных бэкапов через systemd. +- `fail2ban/jail.d/owngit.local` — готовый профиль fail2ban (sshd + recidive). +- `ssh/sshd_config.d/90-owngit-hardening.conf` — шаблон безопасных SSH-настроек. +- `ssh/sshd_config.d/95-owngit-lockdown.conf.example` — опциональный строгий профиль (AllowUsers/Match Address). + +## Архитектура + +- Публичный домен Gitea: `git.papatramp.ru`. +- TLS-сертификат хранится и завершается на HAProxy (`:443`). +- Внутренний доступ к Gitea: `192.168.128.100:3000`. +- Git SSH: внешний `:2222` на HAProxy -> внутренний `192.168.128.100:2222`. + +## 1) Установка Gitea + +```bash +sudo apt update +sudo apt install -y docker.io docker-compose-plugin +sudo systemctl enable --now docker + +sudo mkdir -p /opt/gitea/data/gitea/conf +sudo cp gitea/docker-compose.yml /opt/gitea/docker-compose.yml +cd /opt/gitea +sudo docker compose up -d +``` + +Или one-shot bootstrap: + +```bash +chmod +x scripts/bootstrap-own-git.sh +sudo OWGIT_REPO_DIR=/opt/owngit scripts/bootstrap-own-git.sh +``` + +Сначала безопасная проверка шагов без изменений: + +```bash +OWGIT_REPO_DIR=/opt/owngit scripts/bootstrap-own-git.sh --dry-run +``` + +Полезные переменные bootstrap: + +```bash +SERVER_DOMAIN=git.papatramp.ru +GITEA_LAN_IP=192.168.128.100 +INSTALL_UFW=false +ENABLE_FAIL2BAN=true +ENABLE_BACKUP_TIMER=true +``` + +Положить `app.ini`: + +```bash +sudo cp gitea/app.ini.example /opt/gitea/data/gitea/conf/app.ini +sudo nano /opt/gitea/data/gitea/conf/app.ini +sudo docker compose restart gitea +``` + +Сгенерировать секреты: + +```bash +openssl rand -hex 32 +openssl rand -hex 32 +``` + +## 2) Установка HAProxy конфига + +```bash +sudo cp haproxy/haproxy.cfg /etc/haproxy/haproxy.cfg +sudo haproxy -c -f /etc/haproxy/haproxy.cfg +sudo systemctl reload haproxy +``` + +Важно: сертификат `papatramp.pem` должен включать SAN для `git.papatramp.ru`. + +## 3) Нужно ли SSL на самом Gitea? + +Обычно нет. Достаточно TLS на HAProxy: + +- клиент <-> HAProxy: HTTPS (с сертификатом); +- HAProxy <-> Gitea: HTTP по LAN. + +SSL на Gitea нужен только если нужен end-to-end TLS внутри сети или Gitea публикуется напрямую без HAProxy. + +## 4) Работа и из интернета, и из локалки + +Рекомендуется единый адрес: `https://git.papatramp.ru`. + +Чтобы это работало из LAN: + +- либо Split DNS: `git.papatramp.ru -> LAN IP HAProxy`; +- либо Hairpin NAT на роутере. + +## 5) SSH-клиент (`~/.ssh/config`) + +```sshconfig +Host git-papatramp + HostName git.papatramp.ru + User git + Port 2222 + IdentityFile ~/.ssh/id_ed25519 + IdentitiesOnly yes + ServerAliveInterval 30 + ServerAliveCountMax 6 +``` + +Проверка: + +```bash +ssh -T git-papatramp +``` + +## 6) Миграция репозиториев с GitHub + +Скрипт в `scripts/migrate-github-to-gitea.sh`: + +- получает список реп через GitHub API; +- создает репозитории в Gitea API; +- делает `git clone --mirror` + `git push --mirror`; +- умеет мигрировать LFS (опционально). + +Пример запуска: + +```bash +chmod +x scripts/migrate-github-to-gitea.sh + +export GITHUB_TOKEN='ghp_...' +export GITHUB_OWNER='your_github_user_or_org' +export GITHUB_OWNER_TYPE='user' # или org + +export GITEA_URL='https://git.papatramp.ru' +export GITEA_TOKEN='gitea_pat' +export GITEA_OWNER='your_gitea_user_or_org' +export GITEA_OWNER_TYPE='user' # или org + +export PUSH_WITH_SSH='true' +export GITEA_SSH_HOST='git.papatramp.ru' +export GITEA_SSH_PORT='2222' +export MIGRATE_LFS='true' + +./scripts/migrate-github-to-gitea.sh +``` + +## 7) Быстрые проверки + +```bash +curl -I http://192.168.128.100:3000 +curl -I https://git.papatramp.ru +nc -zv git.papatramp.ru 2222 +``` + +Если всё корректно: + +- web UI открывается по `https://git.papatramp.ru`; +- `ssh -T git-papatramp` достигает Gitea; +- `git clone/push` работает по SSH/HTTPS. + +## 8) Полезные команды через Makefile + +```bash +make help +make check-haproxy +make deploy-haproxy +make gitea-up +make gitea-restart +``` + +Запуск миграции через `.env`: + +```bash +cp .env.example .env +nano .env +make dry-run +make migrate +``` + +## 9) Preflight и backup + +```bash +chmod +x scripts/preflight-check.sh scripts/backup-gitea.sh +./scripts/preflight-check.sh +``` + +Запуск бэкапа вручную: + +```bash +sudo ./scripts/backup-gitea.sh +``` + +Пример cron (ежедневно в 03:10): + +```bash +10 3 * * * /opt/owngit/scripts/backup-gitea.sh >> /var/log/gitea-backup.log 2>&1 +``` + +Восстановление: + +```bash +sudo ./scripts/restore-gitea.sh /opt/backups/gitea/gitea-backup-YYYYMMDD-HHMMSS.tar.gz +``` + +## 10) Автобэкапы через systemd timer + +Установка: + +```bash +sudo cp systemd/gitea-backup.service /etc/systemd/system/gitea-backup.service +sudo cp systemd/gitea-backup.timer /etc/systemd/system/gitea-backup.timer +sudo systemctl daemon-reload +sudo systemctl enable --now gitea-backup.timer +``` + +Проверка: + +```bash +systemctl list-timers --all | grep gitea-backup +sudo systemctl status gitea-backup.timer --no-pager +``` + +## 11) Hardening (firewall + fail2ban) + +```bash +chmod +x scripts/setup-ufw.sh +sudo ./scripts/setup-ufw.sh +``` + +Настройка fail2ban: + +```bash +sudo apt install -y fail2ban +sudo cp fail2ban/jail.d/owngit.local /etc/fail2ban/jail.d/owngit.local +sudo systemctl enable --now fail2ban +sudo fail2ban-client status +``` + +Подробный чеклист и приоритеты см. в `docs/security-hardening.md`. + +Применение SSH hardening: + +```bash +chmod +x scripts/safe-apply-ssh-hardening.sh +sudo ./scripts/safe-apply-ssh-hardening.sh +``` + +Опциональный lockdown: + +```bash +chmod +x scripts/safe-apply-ssh-lockdown.sh +sudo ./scripts/safe-apply-ssh-lockdown.sh +``` diff --git a/docs/security-hardening.md b/docs/security-hardening.md new file mode 100644 index 0000000..844a48e --- /dev/null +++ b/docs/security-hardening.md @@ -0,0 +1,87 @@ +# Security Hardening Checklist + +Этот список рассчитан на инфраструктуру из этого репозитория: + +- HAProxy как edge reverse proxy; +- Gitea в Docker; +- публичные порты `80`, `443`, `2222`, `25`, `3389`. + +## Приоритет P0 (сделать в первую очередь) + +- Обновить систему: + - `sudo apt update && sudo apt full-upgrade -y` +- Включить автообновления security-пакетов (`unattended-upgrades`). +- Сменить/усилить пароли админов, включить 2FA в Gitea. +- Проверить `ROOT_URL=https://git.papatramp.ru/` в `app.ini`. +- Убедиться, что TLS-сертификат на HAProxy содержит SAN `git.papatramp.ru`. +- Ограничить доступ к закрытым сервисам через `allowed_ips.lst`. + +## Приоритет P1 (в течение дня) + +- Настроить firewall (UFW): + - `sudo ./scripts/setup-ufw.sh` +- Настроить fail2ban: + - `sudo apt install -y fail2ban` + - `sudo cp fail2ban/jail.d/owngit.local /etc/fail2ban/jail.d/owngit.local` + - `sudo systemctl enable --now fail2ban` + - `sudo fail2ban-client status` +- Отключить root login по SSH и парольный вход: + - использовать безопасный скрипт: + - `sudo ./scripts/safe-apply-ssh-hardening.sh` + - шаблон конфига: `ssh/sshd_config.d/90-owngit-hardening.conf` + +## Приоритет P2 (регулярная эксплуатация) + +- Включить ежедневные бэкапы (`systemd` timer из `systemd/`). +- Раз в месяц делать тестовое восстановление через `scripts/restore-gitea.sh`. +- Проверять fail2ban: + - `sudo fail2ban-client status sshd` +- Проверять логи HAProxy/Gitea: + - ошибки 4xx/5xx, + - всплески соединений и brute-force. +- Хранить бэкапы вне сервера (NAS/S3/другой хост). + +## Минимальная проверка после hardening + +```bash +sudo ufw status verbose +sudo fail2ban-client status +curl -I https://git.papatramp.ru +ssh -T -p 2222 git@git.papatramp.ru +``` + +## Безопасное применение SSH hardening + +```bash +chmod +x scripts/safe-apply-ssh-hardening.sh +sudo ./scripts/safe-apply-ssh-hardening.sh +``` + +Скрипт: + +- делает backup текущих SSH-конфигов; +- устанавливает include-конфиг в `/etc/ssh/sshd_config.d/`; +- проверяет синтаксис `sshd -t`; +- перезагружает ssh только при валидной конфигурации. + +## Опциональный Lockdown профиль + +Если нужен строгий доступ только для отдельных пользователей/сетей: + +1. Отредактируй шаблон: + - `ssh/sshd_config.d/95-owngit-lockdown.conf.example` +2. Укажи реальные логины в `AllowUsers`. +3. Применяй только безопасным скриптом: + +```bash +chmod +x scripts/safe-apply-ssh-lockdown.sh +sudo ./scripts/safe-apply-ssh-lockdown.sh +``` + +Что делает скрипт: + +- проверяет наличие `AllowUsers` в шаблоне; +- ставит конфиг в `/etc/ssh/sshd_config.d/95-owngit-lockdown.conf`; +- валидирует `sshd -t`; +- делает reload только при валидной конфигурации; +- сохраняет backup предыдущего lockdown-файла. diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md new file mode 100644 index 0000000..0ffbfe2 --- /dev/null +++ b/docs/troubleshooting.md @@ -0,0 +1,121 @@ +# Troubleshooting + +## 1) `https://git.papatramp.ru` не открывается + +Проверь DNS и маршрутизацию: + +```bash +dig +short git.papatramp.ru +``` + +- из интернета должен резолвиться во внешний IP; +- из локалки лучше Split DNS на LAN IP HAProxy. + +Проверь HAProxy: + +```bash +sudo haproxy -c -f /etc/haproxy/haproxy.cfg +sudo systemctl status haproxy --no-pager +``` + +## 2) 404 от HAProxy + +Причина: не совпадает `Host`, запрос уходит в `bk_http_default`. + +Проверка: + +```bash +curl -I -H "Host: git.papatramp.ru" http://127.0.0.1 +``` + +Убедись, что есть ACL: + +- `acl host_git hdr(host) -i git.papatramp.ru` +- `use_backend bk_gitea if host_git` + +## 3) 503 от HAProxy на Gitea + +Причина: backend `bk_gitea` down. + +Проверки: + +```bash +curl -I http://192.168.128.100:3000 +curl -I http://192.168.128.100:3000/api/healthz +``` + +Если не отвечает: + +```bash +cd /opt/gitea +sudo docker compose ps +sudo docker compose logs --tail=200 gitea +``` + +## 4) SSH push не работает (`Permission denied (publickey)`) + +Проверь порт и доступ: + +```bash +nc -zv git.papatramp.ru 2222 +ssh -vvv -p 2222 git@git.papatramp.ru +``` + +Убедись: + +- публичный ключ добавлен в Gitea аккаунт; +- в `~/.ssh/config` правильный `HostName`, `Port`, `IdentityFile`; +- backend `bk_git_ssh` смотрит на правильный IP/порт. + +## 5) В локалке не работает, а из интернета работает + +Обычно это DNS/NAT loopback: + +- настрой Split DNS (`git.papatramp.ru -> LAN IP HAProxy`); +- либо включи Hairpin NAT на роутере. + +## 6) Миграция не создает репозитории в Gitea + +Проверь токен и права: + +- `GITEA_TOKEN` должен иметь право создавать репозитории; +- для org нужен доступ к организации; +- `GITEA_OWNER_TYPE` должен соответствовать `user`/`org`. + +Проверка API: + +```bash +curl -H "Authorization: token $GITEA_TOKEN" "$GITEA_URL/api/v1/user" +``` + +## 7) Private репозитории GitHub не видны в миграции + +Проверь `GITHUB_TOKEN`: + +- токен должен иметь права на чтение приватных репозиториев; +- `GITHUB_OWNER` и `GITHUB_OWNER_TYPE` должны быть корректными. + +## 8) LFS не перенесся + +Включи: + +```bash +export MIGRATE_LFS=true +``` + +И проверь наличие `git-lfs`: + +```bash +git lfs version +``` + +## 9) Кривые редиректы/URL в Gitea + +Проверь `ROOT_URL` в `app.ini`: + +```ini +[server] +ROOT_URL = https://git.papatramp.ru/ +``` + +И заголовки от HAProxy в `bk_gitea` (`X-Forwarded-Proto`, `X-Forwarded-Host`, `Host`). diff --git a/fail2ban/jail.d/owngit.local b/fail2ban/jail.d/owngit.local new file mode 100644 index 0000000..f64c5a9 --- /dev/null +++ b/fail2ban/jail.d/owngit.local @@ -0,0 +1,18 @@ +[DEFAULT] +bantime = 1h +findtime = 10m +maxretry = 5 +backend = systemd + +[sshd] +enabled = true +port = ssh +logpath = %(sshd_log)s +maxretry = 5 + +[recidive] +enabled = true +logpath = /var/log/fail2ban.log +bantime = 1w +findtime = 1d +maxretry = 5 diff --git a/gitea/app.ini.example b/gitea/app.ini.example new file mode 100644 index 0000000..24edb23 --- /dev/null +++ b/gitea/app.ini.example @@ -0,0 +1,39 @@ +APP_NAME = Gitea +RUN_MODE = prod +RUN_USER = git + +[server] +DOMAIN = git.papatramp.ru +SSH_DOMAIN = git.papatramp.ru +HTTP_PORT = 3000 +ROOT_URL = https://git.papatramp.ru/ +DISABLE_SSH = false +START_SSH_SERVER = true +SSH_PORT = 2222 +SSH_LISTEN_PORT = 2222 +PROTOCOL = http + +[service] +DISABLE_REGISTRATION = true +REQUIRE_SIGNIN_VIEW = false +REGISTER_EMAIL_CONFIRM = false +ENABLE_NOTIFY_MAIL = false +ALLOW_ONLY_EXTERNAL_REGISTRATION = false + +[security] +INSTALL_LOCK = true +SECRET_KEY = CHANGE_ME_LONG_RANDOM_SECRET +INTERNAL_TOKEN = CHANGE_ME_LONG_RANDOM_TOKEN +PASSWORD_HASH_ALGO = argon2 + +[database] +DB_TYPE = sqlite3 +PATH = /data/gitea/gitea.db + +[repository] +ROOT = /data/git/repositories + +[log] +MODE = console +LEVEL = Info +ROOT_PATH = /data/gitea/log diff --git a/gitea/docker-compose.yml b/gitea/docker-compose.yml new file mode 100644 index 0000000..d4d7c9a --- /dev/null +++ b/gitea/docker-compose.yml @@ -0,0 +1,16 @@ +services: + gitea: + image: gitea/gitea:latest + container_name: gitea + restart: unless-stopped + environment: + - USER_UID=1000 + - USER_GID=1000 + - TZ=Europe/Moscow + ports: + - "3000:3000" + - "2222:2222" + volumes: + - /opt/gitea/data:/data + - /etc/timezone:/etc/timezone:ro + - /etc/localtime:/etc/localtime:ro diff --git a/haproxy/haproxy.cfg b/haproxy/haproxy.cfg new file mode 100644 index 0000000..eb84f1b --- /dev/null +++ b/haproxy/haproxy.cfg @@ -0,0 +1,199 @@ +# HAProxy — домашняя сеть papatramp.ru +# Версия синтаксиса: HAProxy 2.4+ +# +# TLS завершается на HAProxy. +# Gitea снаружи: git.papatramp.ru +# Gitea внутри: 192.168.128.100 (или git.papatramp.ru, если локально резолвится в LAN IP). + +global + log stdout format raw local0 + maxconn 40000 + # tune.ssl.default-dh-param 2048 + +defaults + mode http + log global + option httplog + option dontlognull + timeout connect 10s + timeout client 300s + timeout server 300s + timeout http-request 10s + +# --- HTTP (80) +frontend fe_http + bind *:80 + mode http + option forwardfor except 127.0.0.0/8 + + acl path_acme path_beg /.well-known/acme-challenge/ + acl trusted src -f /etc/haproxy/allowed_ips.lst + acl trusted src -f /etc/haproxy/allowed_ips_knock.lst + acl host_photo hdr(host) -i photostation.papatramp.ru + acl host_oldbox hdr(host) -i oldbox.papatramp.ru + acl host_git hdr(host) -i git.papatramp.ru + + # ACME не блокируем по trusted + http-request deny deny_status 403 if host_photo !trusted !path_acme + http-request deny deny_status 403 if host_oldbox !trusted !path_acme + + use_backend bk_acme if path_acme + use_backend bk_gitea if host_git + use_backend bk_photostation if host_photo + use_backend bk_oldbox if host_oldbox + use_backend bk_zabbix if { hdr(host) -i zabbix.papatramp.ru } + + default_backend bk_http_default + +# --- HTTPS (443) +frontend fe_https + # Только HTTP/1.1 к клиенту (как в текущем рабочем варианте) + bind *:443 ssl crt /etc/haproxy/certs/papatramp.pem alpn http/1.1 + mode http + option forwardfor except 127.0.0.0/8 + + acl trusted src -f /etc/haproxy/allowed_ips.lst + acl trusted src -f /etc/haproxy/allowed_ips_knock.lst + + acl host_photo hdr(host) -i photostation.papatramp.ru + acl host_oldbox hdr(host) -i oldbox.papatramp.ru + acl host_git hdr(host) -i git.papatramp.ru + + http-request deny deny_status 403 if host_photo !trusted + http-request deny deny_status 403 if host_oldbox !trusted + + use_backend bk_gitea if host_git + use_backend bk_photostation if host_photo + use_backend bk_oldbox if host_oldbox + use_backend bk_zabbix if { hdr(host) -i zabbix.papatramp.ru } + + default_backend bk_http_default + +# Заглушка для неизвестного Host +backend bk_http_default + mode http + http-request deny deny_status 404 + +# ACME webroot через локальный nginx +backend bk_acme + mode http + server nginx_acme 127.0.0.1:8080 + +# Synology Photo Station / DSM +backend bk_photostation + mode http + balance roundrobin + http-request set-header Host 192.168.128.190:5003 + http-request set-header X-Forwarded-Host %[req.hdr(Host)] + http-request set-header X-Forwarded-Proto https if { ssl_fc } + http-request set-header X-Forwarded-Proto http if !{ ssl_fc } + http-request set-header X-Forwarded-Port 443 if { ssl_fc } + http-request set-header X-Forwarded-Port 80 if !{ ssl_fc } + http-response replace-value Location ^https?://192\.168\.128\.190:5003/(.*)$ https://photostation.papatramp.ru/\1 + http-response replace-value Location ^https?://192\.168\.128\.190:5000/(.*)$ https://photostation.papatramp.ru/\1 + http-response replace-value Location ^https?://192\.168\.128\.190/(.*)$ https://photostation.papatramp.ru/\1 + server syno190 192.168.128.190:5003 ssl verify none check inter 10s rise 2 fall 3 + # Вариант только HTTP к NAS: + # server syno190 192.168.128.190:5000 check inter 10s rise 2 fall 3 + +backend bk_oldbox + mode http + balance roundrobin + http-request set-header Host 192.168.128.180:5001 + http-request set-header X-Forwarded-Host %[req.hdr(Host)] + http-request set-header X-Forwarded-Proto https if { ssl_fc } + http-request set-header X-Forwarded-Proto http if !{ ssl_fc } + http-request set-header X-Forwarded-Port 443 if { ssl_fc } + http-request set-header X-Forwarded-Port 80 if !{ ssl_fc } + http-response replace-value Location ^https?://192\.168\.128\.180:5001/(.*)$ https://oldbox.papatramp.ru/\1 + http-response replace-value Location ^https?://192\.168\.128\.180:5000/(.*)$ https://oldbox.papatramp.ru/\1 + http-response replace-value Location ^https?://192\.168\.128\.180/(.*)$ https://oldbox.papatramp.ru/\1 + server syno180 192.168.128.180:5001 ssl verify none check inter 10s rise 2 fall 3 + # server syno180 192.168.128.180:5000 check inter 10s rise 2 fall 3 + +# Zabbix +backend bk_zabbix + mode http + balance roundrobin + option httpchk + http-check send meth GET uri / ver HTTP/1.1 hdr Host 192.168.128.52 + http-check expect rstatus ^[23] + http-request set-header X-Forwarded-Proto https if { ssl_fc } + http-request set-header X-Forwarded-Proto http if !{ ssl_fc } + http-request set-header X-Forwarded-Port 443 if { ssl_fc } + http-request set-header X-Forwarded-Port 80 if !{ ssl_fc } + http-request set-header Host 192.168.128.52 + server zabbix1 192.168.128.52:80 check inter 15s rise 2 fall 3 + +# Gitea/Forgejo UI + Git over HTTP(S) +backend bk_gitea + mode http + option httpchk + http-check send meth GET uri /api/healthz ver HTTP/1.1 hdr Host git.papatramp.ru + http-check expect status 200 + http-request set-header Host git.papatramp.ru + http-request set-header X-Forwarded-Host %[req.hdr(Host)] + http-request set-header X-Forwarded-Proto https if { ssl_fc } + http-request set-header X-Forwarded-Proto http if !{ ssl_fc } + http-request set-header X-Forwarded-Port 443 if { ssl_fc } + http-request set-header X-Forwarded-Port 80 if !{ ssl_fc } + server gitea1 192.168.128.100:3000 check inter 10s rise 2 fall 3 + # Если внутри хочешь ходить по имени: + # server gitea1 git.papatramp.ru:3000 check inter 10s rise 2 fall 3 + +# Git SSH (внешний 2222 -> внутренний Gitea 2222) +frontend fe_git_ssh + bind *:2222 + mode tcp + option tcplog + # При необходимости ограничения по IP: + # acl ssh_trusted src -f /etc/haproxy/allowed_ips.lst + # acl ssh_trusted src -f /etc/haproxy/allowed_ips_knock.lst + # tcp-request connection reject if !ssh_trusted + default_backend bk_git_ssh + +backend bk_git_ssh + mode tcp + server gitea_ssh 192.168.128.100:2222 check inter 10s rise 2 fall 3 + # Вариант через имя: + # server gitea_ssh git.papatramp.ru:2222 check inter 10s rise 2 fall 3 + +# --- RDP (TCP) +frontend fe_rdp_papa + bind *:3389 + mode tcp + option tcplog + acl rdp_trusted src -f /etc/haproxy/allowed_ips.lst + acl rdp_trusted src -f /etc/haproxy/allowed_ips_knock.lst + tcp-request connection reject if !rdp_trusted + default_backend bk_rdp_papa + +backend bk_rdp_papa + mode tcp + server win_papa 192.168.128.5:3389 check inter 5s rise 2 fall 3 + +# Второй RDP (:3390 -> 192.168.128.30) — пример +#frontend fe_rdp_second +# bind *:3390 +# mode tcp +# option tcplog +# acl rdp_trusted src -f /etc/haproxy/allowed_ips.lst +# acl rdp_trusted src -f /etc/haproxy/allowed_ips_knock.lst +# tcp-request connection reject if !rdp_trusted +# default_backend bk_rdp_second +# +#backend bk_rdp_second +# mode tcp +# server win_rdp2 192.168.128.30:3389 check inter 5s rise 2 fall 3 + +# --- SMTP (TCP/25) +frontend fe_smtp + bind *:25 + mode tcp + option tcplog + default_backend bk_smtp + +backend bk_smtp + mode tcp + option smtpchk + server mail_mx 192.168.128.80:25 check inter 15s rise 2 fall 3 diff --git a/scripts/backup-gitea.sh b/scripts/backup-gitea.sh new file mode 100644 index 0000000..823fce3 --- /dev/null +++ b/scripts/backup-gitea.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Резервное копирование Gitea data dir + app.ini с простой ротацией. +# По умолчанию: +# - источник: /opt/gitea/data +# - бэкапы: /opt/backups/gitea +# - хранить: 14 архивов + +GITEA_DATA_DIR="${GITEA_DATA_DIR:-/opt/gitea/data}" +BACKUP_DIR="${BACKUP_DIR:-/opt/backups/gitea}" +RETENTION="${RETENTION:-14}" +DATE_TAG="$(date +%Y%m%d-%H%M%S)" +ARCHIVE_NAME="gitea-backup-${DATE_TAG}.tar.gz" +TMP_DIR="${BACKUP_DIR}/.tmp-${DATE_TAG}" + +mkdir -p "$BACKUP_DIR" +mkdir -p "$TMP_DIR" + +echo "[INFO] Backup started: $DATE_TAG" +echo "[INFO] Source: $GITEA_DATA_DIR" +echo "[INFO] Target: $BACKUP_DIR/$ARCHIVE_NAME" + +if [[ ! -d "$GITEA_DATA_DIR" ]]; then + echo "[ERR] Source directory not found: $GITEA_DATA_DIR" + exit 1 +fi + +# Копируем только нужное в staging, чтобы архив был чистым. +cp -a "$GITEA_DATA_DIR" "$TMP_DIR/data" + +tar -C "$TMP_DIR" -czf "${BACKUP_DIR}/${ARCHIVE_NAME}" data +rm -rf "$TMP_DIR" + +echo "[OK] Archive created: ${BACKUP_DIR}/${ARCHIVE_NAME}" + +# Ротация по количеству файлов (новые сохраняем, старые удаляем). +mapfile -t OLD_BACKUPS < <(ls -1t "${BACKUP_DIR}"/gitea-backup-*.tar.gz 2>/dev/null || true) +COUNT="${#OLD_BACKUPS[@]}" +if (( COUNT > RETENTION )); then + echo "[INFO] Retention: keep newest ${RETENTION}, remove $((COUNT - RETENTION))" + for (( i=RETENTION; i /etc/haproxy/certs/papatramp.pem" + run_cmd rm -f /tmp/owngit-selfsigned.crt /tmp/owngit-selfsigned.key + warn "Используется временный сертификат. Замените на боевой Let's Encrypt/PEM." + fi + + run_cmd haproxy -c -f /etc/haproxy/haproxy.cfg + run_cmd systemctl enable --now haproxy + run_cmd systemctl reload haproxy + ok "HAProxy применен и перезагружен" +} + +deploy_fail2ban() { + if [[ "$ENABLE_FAIL2BAN" != "true" ]]; then + warn "Fail2ban пропущен (ENABLE_FAIL2BAN=false)" + return + fi + log "Настраиваю fail2ban..." + run_cmd apt-get install -y fail2ban + run_cmd mkdir -p /etc/fail2ban/jail.d + run_cmd cp -a "$OWGIT_REPO_DIR/fail2ban/jail.d/owngit.local" /etc/fail2ban/jail.d/owngit.local + run_cmd systemctl enable --now fail2ban + ok "Fail2ban включен" +} + +deploy_backup_timer() { + if [[ "$ENABLE_BACKUP_TIMER" != "true" ]]; then + warn "Backup timer пропущен (ENABLE_BACKUP_TIMER=false)" + return + fi + log "Устанавливаю systemd backup timer..." + run_cmd cp -a "$OWGIT_REPO_DIR/systemd/gitea-backup.service" /etc/systemd/system/gitea-backup.service + run_cmd cp -a "$OWGIT_REPO_DIR/systemd/gitea-backup.timer" /etc/systemd/system/gitea-backup.timer + run_cmd systemctl daemon-reload + run_cmd systemctl enable --now gitea-backup.timer + ok "Backup timer включен" +} + +maybe_setup_ufw() { + if [[ "$INSTALL_UFW" != "true" ]]; then + warn "UFW не применялся (INSTALL_UFW=false)" + return + fi + if [[ ! -x "$OWGIT_REPO_DIR/scripts/setup-ufw.sh" ]]; then + run_cmd chmod +x "$OWGIT_REPO_DIR/scripts/setup-ufw.sh" + fi + log "Применяю UFW..." + run_cmd "$OWGIT_REPO_DIR/scripts/setup-ufw.sh" + ok "UFW настроен" +} + +print_checklist() { + cat < Gitea. +# Переносит всю git-историю через mirror push. Может переносить LFS (опционально). + +: "${GITHUB_TOKEN:?Set GITHUB_TOKEN}" +: "${GITHUB_OWNER:?Set GITHUB_OWNER (user or org on GitHub)}" +: "${GITHUB_OWNER_TYPE:?Set GITHUB_OWNER_TYPE=user|org}" + +: "${GITEA_URL:?Set GITEA_URL, e.g. https://git.papatramp.ru}" +: "${GITEA_TOKEN:?Set GITEA_TOKEN (Gitea PAT)}" +: "${GITEA_OWNER:?Set GITEA_OWNER (target user/org in Gitea)}" +: "${GITEA_OWNER_TYPE:?Set GITEA_OWNER_TYPE=user|org}" + +WORKDIR="${WORKDIR:-$PWD/migration-workdir}" +PUSH_WITH_SSH="${PUSH_WITH_SSH:-true}" # true/false +GITEA_SSH_HOST="${GITEA_SSH_HOST:-git.papatramp.ru}" +GITEA_SSH_PORT="${GITEA_SSH_PORT:-2222}" +MIGRATE_LFS="${MIGRATE_LFS:-false}" # true/false + +mkdir -p "$WORKDIR" + +need_cmd() { command -v "$1" >/dev/null 2>&1 || { echo "Missing command: $1"; exit 1; }; } +need_cmd curl +need_cmd jq +need_cmd git + +if [[ "$MIGRATE_LFS" == "true" ]]; then + need_cmd git-lfs +fi + +gh_api() { + local url="$1" + curl -fsSL \ + -H "Authorization: Bearer $GITHUB_TOKEN" \ + -H "Accept: application/vnd.github+json" \ + "$url" +} + +gitea_api() { + local method="$1" + local url="$2" + local data="${3:-}" + if [[ -n "$data" ]]; then + curl -sS -o /tmp/gitea_resp.json -w "%{http_code}" \ + -X "$method" \ + -H "Authorization: token $GITEA_TOKEN" \ + -H "Content-Type: application/json" \ + -d "$data" \ + "$url" + else + curl -sS -o /tmp/gitea_resp.json -w "%{http_code}" \ + -X "$method" \ + -H "Authorization: token $GITEA_TOKEN" \ + "$url" + fi +} + +create_gitea_repo_if_needed() { + local repo_name="$1" + local private="$2" + local payload endpoint code + + payload="$(jq -n --arg name "$repo_name" --argjson private "$private" \ + '{name:$name, private:$private, auto_init:false}')" + + if [[ "$GITEA_OWNER_TYPE" == "org" ]]; then + endpoint="$GITEA_URL/api/v1/orgs/$GITEA_OWNER/repos" + else + endpoint="$GITEA_URL/api/v1/user/repos" + fi + + code="$(gitea_api POST "$endpoint" "$payload")" + if [[ "$code" == "201" ]]; then + echo " [OK] Created: $GITEA_OWNER/$repo_name" + elif [[ "$code" == "409" ]]; then + echo " [SKIP] Exists: $GITEA_OWNER/$repo_name" + else + echo " [ERR] Create repo failed: HTTP $code" + cat /tmp/gitea_resp.json + return 1 + fi +} + +push_mirror() { + local mirror_dir="$1" + local repo_name="$2" + local remote_url + + if [[ "$PUSH_WITH_SSH" == "true" ]]; then + remote_url="ssh://git@${GITEA_SSH_HOST}:${GITEA_SSH_PORT}/${GITEA_OWNER}/${repo_name}.git" + else + : "${GITEA_USERNAME:?Set GITEA_USERNAME when PUSH_WITH_SSH=false}" + remote_url="https://${GITEA_USERNAME}:${GITEA_TOKEN}@${GITEA_URL#https://}/${GITEA_OWNER}/${repo_name}.git" + fi + + if git -C "$mirror_dir" remote | grep -q '^gitea$'; then + git -C "$mirror_dir" remote set-url gitea "$remote_url" + else + git -C "$mirror_dir" remote add gitea "$remote_url" + fi + + git -C "$mirror_dir" push --mirror gitea +} + +migrate_repo() { + local full_name="$1" + local private="$2" + local repo_name="${full_name##*/}" + local mirror_dir="$WORKDIR/${repo_name}.git" + + echo "=== $full_name (private=$private) ===" + rm -rf "$mirror_dir" + + git -c http.extraHeader="Authorization: Bearer $GITHUB_TOKEN" \ + clone --mirror "https://github.com/${full_name}.git" "$mirror_dir" + + create_gitea_repo_if_needed "$repo_name" "$private" + push_mirror "$mirror_dir" "$repo_name" + + if [[ "$MIGRATE_LFS" == "true" ]]; then + git -C "$mirror_dir" lfs fetch --all || true + git -C "$mirror_dir" lfs push --all gitea || true + fi + + echo " [DONE] $full_name" +} + +main() { + local page=1 + local per_page=100 + local url repos_json count + + while true; do + if [[ "$GITHUB_OWNER_TYPE" == "org" ]]; then + url="https://api.github.com/orgs/${GITHUB_OWNER}/repos?type=all&per_page=${per_page}&page=${page}" + else + url="https://api.github.com/users/${GITHUB_OWNER}/repos?type=owner&per_page=${per_page}&page=${page}" + fi + + repos_json="$(gh_api "$url")" + count="$(echo "$repos_json" | jq 'length')" + [[ "$count" -eq 0 ]] && break + + while IFS=$'\t' read -r full_name private; do + migrate_repo "$full_name" "$private" + done < <(echo "$repos_json" | jq -r '.[] | [.full_name, .private] | @tsv') + + ((page++)) + done + + echo "All repositories migrated." +} + +main diff --git a/scripts/preflight-check.sh b/scripts/preflight-check.sh new file mode 100644 index 0000000..e1b943e --- /dev/null +++ b/scripts/preflight-check.sh @@ -0,0 +1,101 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Быстрый preflight для проверки готовности own-git инфраструктуры. +# Можно запускать локально или на сервере. + +GIT_DOMAIN="${GIT_DOMAIN:-git.papatramp.ru}" +GITEA_LAN_IP="${GITEA_LAN_IP:-192.168.128.100}" +GITEA_HTTP_PORT="${GITEA_HTTP_PORT:-3000}" +GITEA_SSH_PORT="${GITEA_SSH_PORT:-2222}" +HA_PROXY_HTTP_PORT="${HA_PROXY_HTTP_PORT:-80}" +HA_PROXY_HTTPS_PORT="${HA_PROXY_HTTPS_PORT:-443}" + +have_cmd() { command -v "$1" >/dev/null 2>&1; } + +check_port() { + local host="$1" + local port="$2" + if have_cmd nc; then + nc -z -w 3 "$host" "$port" >/dev/null 2>&1 + elif have_cmd timeout; then + timeout 3 bash -c "cat < /dev/null > /dev/tcp/$host/$port" >/dev/null 2>&1 + else + echo " [WARN] neither nc nor timeout available, skip port test $host:$port" + return 0 + fi +} + +ok() { echo "[OK] $*"; } +warn() { echo "[WARN] $*"; } +err() { echo "[ERR] $*"; } + +echo "== own-git preflight ==" +echo "GIT_DOMAIN=$GIT_DOMAIN" +echo "GITEA_LAN_IP=$GITEA_LAN_IP" +echo + +if have_cmd dig; then + DNS_RESULT="$(dig +short "$GIT_DOMAIN" | tr '\n' ' ' || true)" + if [[ -n "${DNS_RESULT// }" ]]; then + ok "DNS resolve: $GIT_DOMAIN -> $DNS_RESULT" + else + warn "DNS empty for $GIT_DOMAIN" + fi +elif have_cmd getent; then + DNS_RESULT="$(getent hosts "$GIT_DOMAIN" || true)" + if [[ -n "$DNS_RESULT" ]]; then + ok "DNS resolve via getent: $DNS_RESULT" + else + warn "DNS empty for $GIT_DOMAIN" + fi +else + warn "no dig/getent, DNS check skipped" +fi + +if check_port "$GIT_DOMAIN" "$HA_PROXY_HTTP_PORT"; then + ok "Port open: $GIT_DOMAIN:$HA_PROXY_HTTP_PORT" +else + warn "Port closed/unreachable: $GIT_DOMAIN:$HA_PROXY_HTTP_PORT" +fi + +if check_port "$GIT_DOMAIN" "$HA_PROXY_HTTPS_PORT"; then + ok "Port open: $GIT_DOMAIN:$HA_PROXY_HTTPS_PORT" +else + warn "Port closed/unreachable: $GIT_DOMAIN:$HA_PROXY_HTTPS_PORT" +fi + +if check_port "$GIT_DOMAIN" "$GITEA_SSH_PORT"; then + ok "Port open: $GIT_DOMAIN:$GITEA_SSH_PORT (SSH through HAProxy)" +else + warn "Port closed/unreachable: $GIT_DOMAIN:$GITEA_SSH_PORT" +fi + +if have_cmd curl; then + if curl -fsS -m 5 "http://$GITEA_LAN_IP:$GITEA_HTTP_PORT/api/healthz" >/dev/null; then + ok "Gitea LAN healthz OK: http://$GITEA_LAN_IP:$GITEA_HTTP_PORT/api/healthz" + else + warn "Gitea LAN healthz failed: http://$GITEA_LAN_IP:$GITEA_HTTP_PORT/api/healthz" + fi + + if curl -fsS -m 8 "https://$GIT_DOMAIN/api/healthz" >/dev/null; then + ok "Public healthz OK: https://$GIT_DOMAIN/api/healthz" + else + warn "Public healthz failed: https://$GIT_DOMAIN/api/healthz" + fi +else + warn "curl not found, HTTP checks skipped" +fi + +if have_cmd ssh; then + if ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ConnectTimeout=5 -p "$GITEA_SSH_PORT" "git@$GIT_DOMAIN" exit >/dev/null 2>&1; then + ok "SSH handshake OK (auth may still be key-dependent)" + else + warn "SSH check failed or key not configured: ssh -p $GITEA_SSH_PORT git@$GIT_DOMAIN" + fi +else + warn "ssh not found, SSH check skipped" +fi + +echo +echo "Preflight complete." diff --git a/scripts/restore-gitea.sh b/scripts/restore-gitea.sh new file mode 100644 index 0000000..bdb30a0 --- /dev/null +++ b/scripts/restore-gitea.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Восстановление Gitea из архива backup-gitea.sh. +# +# Пример: +# sudo ./scripts/restore-gitea.sh /opt/backups/gitea/gitea-backup-20260421-031000.tar.gz +# +# По умолчанию: +# - target dir: /opt/gitea/data +# - create snapshot before restore: yes + +if [[ $# -lt 1 ]]; then + echo "Usage: $0 " + exit 1 +fi + +ARCHIVE_PATH="$1" +TARGET_DIR="${TARGET_DIR:-/opt/gitea/data}" +TAKE_SNAPSHOT_BEFORE_RESTORE="${TAKE_SNAPSHOT_BEFORE_RESTORE:-true}" +SNAPSHOT_DIR="${SNAPSHOT_DIR:-/opt/backups/gitea}" +DATE_TAG="$(date +%Y%m%d-%H%M%S)" + +if [[ ! -f "$ARCHIVE_PATH" ]]; then + echo "[ERR] Archive not found: $ARCHIVE_PATH" + exit 1 +fi + +if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then + echo "[ERR] Run as root (sudo)." + exit 1 +fi + +echo "[INFO] Restore archive: $ARCHIVE_PATH" +echo "[INFO] Target dir: $TARGET_DIR" + +if [[ "$TAKE_SNAPSHOT_BEFORE_RESTORE" == "true" && -d "$TARGET_DIR" ]]; then + mkdir -p "$SNAPSHOT_DIR" + SNAPSHOT_PATH="${SNAPSHOT_DIR}/gitea-pre-restore-${DATE_TAG}.tar.gz" + echo "[INFO] Creating pre-restore snapshot: $SNAPSHOT_PATH" + tar -C "$(dirname "$TARGET_DIR")" -czf "$SNAPSHOT_PATH" "$(basename "$TARGET_DIR")" +fi + +if command -v docker >/dev/null 2>&1; then + if [[ -f /opt/gitea/docker-compose.yml ]]; then + echo "[INFO] Stopping Gitea container..." + (cd /opt/gitea && docker compose stop gitea) + fi +fi + +TMP_DIR="$(mktemp -d)" +trap 'rm -rf "$TMP_DIR"' EXIT + +echo "[INFO] Extracting archive..." +tar -C "$TMP_DIR" -xzf "$ARCHIVE_PATH" + +if [[ ! -d "$TMP_DIR/data" ]]; then + echo "[ERR] Archive structure is invalid: expected 'data/' root folder" + exit 1 +fi + +echo "[INFO] Restoring files to $TARGET_DIR" +rm -rf "$TARGET_DIR" +mkdir -p "$(dirname "$TARGET_DIR")" +mv "$TMP_DIR/data" "$TARGET_DIR" + +# Чаще всего Gitea в контейнере работает как uid/gid 1000. +chown -R 1000:1000 "$TARGET_DIR" || true + +if command -v docker >/dev/null 2>&1; then + if [[ -f /opt/gitea/docker-compose.yml ]]; then + echo "[INFO] Starting Gitea container..." + (cd /opt/gitea && docker compose up -d gitea) + fi +fi + +echo "[OK] Restore completed" diff --git a/scripts/safe-apply-ssh-hardening.sh b/scripts/safe-apply-ssh-hardening.sh new file mode 100644 index 0000000..b319f3a --- /dev/null +++ b/scripts/safe-apply-ssh-hardening.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Безопасное применение SSH hardening-конфига. +# Делает backup текущих конфигов, ставит include-конфиг, валидирует `sshd -t` +# и перезагружает ssh только если конфиг валиден. +# +# ВАЖНО: +# - запускать из уже открытой SSH-сессии не страшно, но не закрывайте текущую +# сессию, пока не проверите вход новой. + +SOURCE_CONF="${SOURCE_CONF:-ssh/sshd_config.d/90-owngit-hardening.conf}" +TARGET_DIR="${TARGET_DIR:-/etc/ssh/sshd_config.d}" +TARGET_CONF="${TARGET_DIR}/90-owngit-hardening.conf" +BACKUP_DIR="${BACKUP_DIR:-/root/ssh-hardening-backups}" +DATE_TAG="$(date +%Y%m%d-%H%M%S)" +MGMT_SSH_PORT="${MGMT_SSH_PORT:-22}" + +if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then + echo "[ERR] Run as root (sudo)." + exit 1 +fi + +if [[ ! -f "$SOURCE_CONF" ]]; then + echo "[ERR] Source file not found: $SOURCE_CONF" + exit 1 +fi + +if ! command -v sshd >/dev/null 2>&1; then + echo "[ERR] sshd not found." + exit 1 +fi + +mkdir -p "$TARGET_DIR" "$BACKUP_DIR" + +echo "[INFO] Backing up current ssh config..." +if [[ -f /etc/ssh/sshd_config ]]; then + cp -a /etc/ssh/sshd_config "${BACKUP_DIR}/sshd_config.${DATE_TAG}.bak" +fi +if [[ -d /etc/ssh/sshd_config.d ]]; then + tar -C /etc/ssh -czf "${BACKUP_DIR}/sshd_config.d.${DATE_TAG}.tar.gz" sshd_config.d +fi + +echo "[INFO] Installing hardening config to ${TARGET_CONF}" +cp -a "$SOURCE_CONF" "$TARGET_CONF" +chmod 0644 "$TARGET_CONF" + +echo "[INFO] Ensuring management SSH port ${MGMT_SSH_PORT} remains allowed in effective config" +if ! sshd -T | grep -q "^port ${MGMT_SSH_PORT}$"; then + echo "[WARN] Effective sshd port is not ${MGMT_SSH_PORT}. Check your base sshd_config." +fi + +echo "[INFO] Validating sshd config..." +if sshd -t; then + echo "[OK] sshd config is valid" +else + echo "[ERR] sshd config test failed, restoring previous state" + rm -f "$TARGET_CONF" + if [[ -f "${BACKUP_DIR}/sshd_config.${DATE_TAG}.bak" ]]; then + cp -a "${BACKUP_DIR}/sshd_config.${DATE_TAG}.bak" /etc/ssh/sshd_config + fi + exit 1 +fi + +echo "[INFO] Reloading ssh service..." +if systemctl reload ssh; then + echo "[OK] ssh reloaded" +elif systemctl reload sshd; then + echo "[OK] sshd reloaded" +else + echo "[ERR] failed to reload ssh service" + exit 1 +fi + +echo +echo "[NEXT] Verify from a new terminal before closing current session:" +echo " ssh -p ${MGMT_SSH_PORT} @" +echo "If login fails, restore backups from ${BACKUP_DIR}." diff --git a/scripts/safe-apply-ssh-lockdown.sh b/scripts/safe-apply-ssh-lockdown.sh new file mode 100644 index 0000000..d05532e --- /dev/null +++ b/scripts/safe-apply-ssh-lockdown.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Безопасное применение SSH lockdown-профиля (AllowUsers/Match Address). +# +# По умолчанию использует шаблон: +# ssh/sshd_config.d/95-owngit-lockdown.conf.example +# и устанавливает как: +# /etc/ssh/sshd_config.d/95-owngit-lockdown.conf +# +# Перед запуском обязательно отредактируйте пользователей в шаблоне. + +SOURCE_CONF="${SOURCE_CONF:-ssh/sshd_config.d/95-owngit-lockdown.conf.example}" +TARGET_DIR="${TARGET_DIR:-/etc/ssh/sshd_config.d}" +TARGET_CONF="${TARGET_DIR}/95-owngit-lockdown.conf" +BACKUP_DIR="${BACKUP_DIR:-/root/ssh-hardening-backups}" +DATE_TAG="$(date +%Y%m%d-%H%M%S)" +REQUIRE_ALLOWUSERS="${REQUIRE_ALLOWUSERS:-true}" + +if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then + echo "[ERR] Run as root (sudo)." + exit 1 +fi + +if [[ ! -f "$SOURCE_CONF" ]]; then + echo "[ERR] Source file not found: $SOURCE_CONF" + exit 1 +fi + +if ! command -v sshd >/dev/null 2>&1; then + echo "[ERR] sshd not found." + exit 1 +fi + +if [[ "$REQUIRE_ALLOWUSERS" == "true" ]]; then + if ! grep -Eq '^[[:space:]]*AllowUsers[[:space:]]+[a-zA-Z0-9._-]+' "$SOURCE_CONF"; then + echo "[ERR] SOURCE_CONF must contain non-empty AllowUsers line before apply." + echo " Edit: $SOURCE_CONF" + exit 1 + fi +fi + +mkdir -p "$TARGET_DIR" "$BACKUP_DIR" + +echo "[INFO] Backing up current sshd include config..." +if [[ -f "$TARGET_CONF" ]]; then + cp -a "$TARGET_CONF" "${BACKUP_DIR}/95-owngit-lockdown.conf.${DATE_TAG}.bak" +fi + +echo "[INFO] Installing lockdown config -> $TARGET_CONF" +cp -a "$SOURCE_CONF" "$TARGET_CONF" +chmod 0644 "$TARGET_CONF" + +echo "[INFO] Validating sshd config..." +if ! sshd -t; then + echo "[ERR] sshd -t failed, rolling back" + if [[ -f "${BACKUP_DIR}/95-owngit-lockdown.conf.${DATE_TAG}.bak" ]]; then + cp -a "${BACKUP_DIR}/95-owngit-lockdown.conf.${DATE_TAG}.bak" "$TARGET_CONF" + else + rm -f "$TARGET_CONF" + fi + exit 1 +fi + +echo "[INFO] Reloading ssh service..." +if systemctl reload ssh; then + echo "[OK] ssh reloaded" +elif systemctl reload sshd; then + echo "[OK] sshd reloaded" +else + echo "[ERR] failed to reload ssh service" + exit 1 +fi + +echo +echo "[NEXT] Open a NEW terminal and verify access first:" +echo " ssh @" +echo "If access fails, restore file from $BACKUP_DIR." diff --git a/scripts/setup-ufw.sh b/scripts/setup-ufw.sh new file mode 100644 index 0000000..095984a --- /dev/null +++ b/scripts/setup-ufw.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Базовая настройка UFW под схему own-git. +# ВНИМАНИЕ: проверьте, что доступ к серверу по SSH есть до включения firewall. + +ALLOW_SSH_PORT="${ALLOW_SSH_PORT:-22}" +ALLOW_GITEA_SSH_PORT="${ALLOW_GITEA_SSH_PORT:-2222}" +ALLOW_HTTP="${ALLOW_HTTP:-true}" +ALLOW_HTTPS="${ALLOW_HTTPS:-true}" +ALLOW_SMTP="${ALLOW_SMTP:-true}" +ALLOW_RDP="${ALLOW_RDP:-true}" +RDP_PORT="${RDP_PORT:-3389}" + +if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then + echo "[ERR] Run as root (sudo)." + exit 1 +fi + +if ! command -v ufw >/dev/null 2>&1; then + echo "[INFO] Installing ufw..." + apt-get update + apt-get install -y ufw +fi + +echo "[INFO] Reset UFW rules" +ufw --force reset + +echo "[INFO] Set default policy" +ufw default deny incoming +ufw default allow outgoing + +echo "[INFO] Allow SSH management port: ${ALLOW_SSH_PORT}/tcp" +ufw allow "${ALLOW_SSH_PORT}/tcp" + +echo "[INFO] Allow Gitea SSH via HAProxy: ${ALLOW_GITEA_SSH_PORT}/tcp" +ufw allow "${ALLOW_GITEA_SSH_PORT}/tcp" + +if [[ "$ALLOW_HTTP" == "true" ]]; then + ufw allow 80/tcp +fi + +if [[ "$ALLOW_HTTPS" == "true" ]]; then + ufw allow 443/tcp +fi + +if [[ "$ALLOW_SMTP" == "true" ]]; then + ufw allow 25/tcp +fi + +if [[ "$ALLOW_RDP" == "true" ]]; then + ufw allow "${RDP_PORT}/tcp" +fi + +echo "[INFO] Enable UFW" +ufw --force enable + +echo "[OK] UFW configured" +ufw status verbose diff --git a/ssh/sshd_config.d/90-owngit-hardening.conf b/ssh/sshd_config.d/90-owngit-hardening.conf new file mode 100644 index 0000000..7d37c43 --- /dev/null +++ b/ssh/sshd_config.d/90-owngit-hardening.conf @@ -0,0 +1,26 @@ +# OwnGit SSH hardening baseline. +# Этот файл рассчитан на OpenSSH в Ubuntu 24.04 (sshd_config.d include). + +# Запрет логина root по SSH. +PermitRootLogin no + +# Только ключевая аутентификация. +PasswordAuthentication no +KbdInteractiveAuthentication no +PubkeyAuthentication yes + +# Ограничение попыток. +MaxAuthTries 4 +LoginGraceTime 30 + +# Не принимать пустые пароли. +PermitEmptyPasswords no + +# Ограничить пробросы по умолчанию. +AllowTcpForwarding no +X11Forwarding no +AllowAgentForwarding no + +# Keepalive для очистки "висящих" соединений. +ClientAliveInterval 300 +ClientAliveCountMax 2 diff --git a/ssh/sshd_config.d/95-owngit-lockdown.conf.example b/ssh/sshd_config.d/95-owngit-lockdown.conf.example new file mode 100644 index 0000000..ac2e0b1 --- /dev/null +++ b/ssh/sshd_config.d/95-owngit-lockdown.conf.example @@ -0,0 +1,23 @@ +# Optional SSH lockdown profile (EXAMPLE). +# Скопируйте в /etc/ssh/sshd_config.d/95-owngit-lockdown.conf и адаптируйте под себя. +# +# ВНИМАНИЕ: +# - сначала проверьте, что у перечисленных пользователей есть рабочие SSH-ключи; +# - применяйте через scripts/safe-apply-ssh-lockdown.sh; +# - не закрывайте текущую сессию, пока не проверите вход новой. + +# Разрешить вход только выбранным пользователям. +AllowUsers youradmin anotheradmin + +# Опционально: ограничить, в каких сетях разрешен вход. +# Пример: разрешить admin только из домашней подсети. +# Match Address 192.168.128.0/24 +# AllowUsers youradmin + +# Пример: явный запрет SSH с публичного интернета. +# Match Address 0.0.0.0/0 +# DenyUsers * + +# Дополнительное ужесточение: +PermitTunnel no +GatewayPorts no diff --git a/systemd/gitea-backup.service b/systemd/gitea-backup.service new file mode 100644 index 0000000..21417a1 --- /dev/null +++ b/systemd/gitea-backup.service @@ -0,0 +1,13 @@ +[Unit] +Description=Run Gitea backup script +After=network-online.target +Wants=network-online.target + +[Service] +Type=oneshot +ExecStart=/opt/owngit/scripts/backup-gitea.sh +User=root +Group=root + +[Install] +WantedBy=multi-user.target diff --git a/systemd/gitea-backup.timer b/systemd/gitea-backup.timer new file mode 100644 index 0000000..5d6386a --- /dev/null +++ b/systemd/gitea-backup.timer @@ -0,0 +1,11 @@ +[Unit] +Description=Daily Gitea backup timer + +[Timer] +OnCalendar=*-*-* 03:10:00 +Persistent=true +RandomizedDelaySec=3m +Unit=gitea-backup.service + +[Install] +WantedBy=timers.target