bootstrap-new-server
Skill1 starsChanged 3 months agoMITView on GitHub
Первичная настройка свежего VPS (Ubuntu 22.04+/Debian 12+): SSH-ключи, отключение root-логина, UFW (deny-in + 22/80/443), fail2ban, Docker + compose, структура /opt/, git с gitleaks pre-commit hook, шаблоны ADR/runbook/incident. Только свежие VPS. Триггеры: «настрой новый сервер», «получил VPS», «свежий ubuntu», «bootstrap server», «поднять чистый сервер», «как у тебя на проде, только мне». НЕ для приведения хаоса в порядок (для этого — cleanup-existing-server); НЕ для серверов с существующими Docker/nginx/configs — это другой жанр.
- Reads credentials
In this repository25 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- audit-securitySkill
- cleanup-existing-serverSkill
- configure-vpn-routingSkill
- deploy-serviceSkill
- devSkill
- extract-subscription-serversSkill
- finalize-vpn-routingSkill
- generate-client-configSkill
- health-checkSkill
- install-monitoring-stackSkill
- inventory-scanSkill
- migrate-server-to-serverSkill
- refresh-vpn-knowledgeSkill
- restore-from-backupSkill
- retroSkill
- rotate-secretsSkill
- second-opinionSkill
- setup-backupsSkill
- setup-happ-subscriptionSkill
- setup-secrets-vaultSkill
- setup-server-proxySkill
- setup-vpn-panelSkill
- sysadmin-initSkill
- sysadmin-meetSkill
The file
Files
<role>
Я провожу за оператора полную первичную настройку свежего VPS — то, что обычно делается долго руками и часто оставляет грабли. После моей работы сервер готов принимать первые контейнеры и инициализирован git-репозиторий с защитой от случайного коммита секретов.
</role><context>
Предполагается:
- Свежий VPS (Ubuntu 22.04+ / Debian 12+).
- Root-доступ через SSH (или sudo для не-root пользователя).
- IP-адрес сервера известен.
- У оператора есть публичный SSH-ключ (`~/.ssh/id_ed25519.pub` или аналогичный).
- (Опционально) Домен резолвится в IP — для будущего nginx/TLS, но не обязательно сейчас.НЕ предполагается:
- Существующие docker-контейнеры или нестандартные конфиги (для этого —
cleanup-existing-server). - Доступ через панель провайдера для recovery — если SSH сломается, у оператора должен быть альтернативный способ восстановления.
</context>
<goals>
После выполнения должно стать TRUE:
- SSH работает только по ключу, root-логин отключён, fail2ban защищает от brute-force.
- UFW: deny-in по умолчанию, allow 22 (или альтернативный порт), 80, 443.
- Docker и docker-compose установлены, тестовый `hello-world` работает.
- Структура `/opt/` готова: `monitoring/`, `admin-panel/`, `shared-db/`, `apps/` (пустые, но созданы).
- Git-репозиторий инициализирован в `/opt/infra` с `.gitignore` и pre-commit hook gitleaks.
- Шаблоны `decisions/0000-template.md`, `incidents/_template.md`, `runbooks/00-template.md` скопированы.
- Базовый `inventory/README.md` создан с заглушками — для дальнейшего заполнения.
</goals>Параметры
| Параметр | Default | Описание |
SERVER_IP | (required) | IP-адрес свежего VPS |
SSH_PORT | 22 | SSH-порт (можно сменить на нестандартный — см. Failed Attempts) |
ADMIN_USER | (required) | Имя пользователя для не-root-доступа (получит sudo) |
SSH_KEY_PUB | (required) | Содержимое публичного ключа оператора |
TIMEZONE | UTC | Таймзона сервера. UTC рекомендуется для серверов (логи коррелируются с другими сервисами без mental-конвертации); если оператор работает в одной локальной зоне и предпочитает её — переопределяется (например, Europe/Moscow, Europe/Berlin, America/New_York). |
INFRA_DIR | /opt/infra | Куда инициализировать git-репозиторий |
SERVER_HOST | autodetect | Имя хоста (если не передано — берётся из hostname) |
Процедура
Шаг 0: Pre-check
Проверить, что сервер свежий и параметры заполнены.
# На локальной машине оператора:
ssh root@$SERVER_IP "uname -a && cat /etc/os-release | grep -E '^(NAME|VERSION)='"
ssh root@$SERVER_IP "docker --version 2>/dev/null && echo 'Docker уже стоит — это НЕ свежий сервер!'"Опциональное чтение agent-config.json (мозг). На свежем сервере конфига обычно нет — это норма (агент ещё не настроен под этого оператора). Скилл — OPTIONAL-режим: если конфиг есть локально (например, оператор уже настраивал агент для другого сервера), скилл подхватит из него language и operator.timezone.
Используй общий helper _lib/find-config.sh в режиме silent — отсутствие
конфига нормально для bootstrap (часто первая операция на свежей машине,
до /sysadmin-init). $SYSADMIN_ROOT запоминается на Шаге 1 Cold Start.
source "$SYSADMIN_ROOT/.claude/skills/_lib/find-config.sh"
# silent: без WARN, $CONFIG="" если не найден
find_sysadmin_config silent
find_brain_config || true # ставит $BRAIN_CONFIG = agent-config.json (агент-поля)
# Агент-поля (ADR-0013): operator.timezone и язык живут в мозге ($BRAIN_CONFIG).
# Legacy-совместимость: если мозга нет — берём из $CONFIG (старый единый формат).
# language переехал в .operator.language (мозг); в legacy — top-level .language.
get_agent_field() { # $1=jq-путь-в-мозге, $2=jq-путь-в-legacy, $3=default
local v=""
[ -n "${BRAIN_CONFIG:-}" ] && v=$(jq -r "$1 // empty" "$BRAIN_CONFIG" 2>/dev/null)
[ -z "$v" ] && [ -n "${CONFIG:-}" ] && [ -f "${CONFIG:-}" ] && v=$(jq -r "$2 // empty" "$CONFIG" 2>/dev/null)
[ -z "$v" ] && v="$3"
echo "$v"
}
# CLI-override > конфиг > defaults
TIMEZONE="${TIMEZONE:-$(get_agent_field '.operator.timezone' '.operator.timezone' UTC)}"
LANG_FROM_CONFIG=$(get_agent_field '.operator.language' '.language' ru)
# LANG_FROM_CONFIG используется только для локализации сообщений скиллаБез конфига скилл работает как раньше — defaults TIMEZONE=UTC, сообщения на русском. Это сознательно: bootstrap часто первая операция на свежей машине, до /sysadmin-init.
Verify:
- ОС — Ubuntu 22.04+/24.04+ или Debian 12+.
- Docker НЕ установлен (иначе — стоп, использовать
cleanup-existing-server). - Параметры
ADMIN_USER,SSH_KEY_PUBзаполнены,SERVER_IPдоступен по SSH.
Если pre-check не прошёл: STOP, доложить оператору. Не модифицировать сервер «из лучших побуждений».
Шаг 1: SSH-hardening (scripts/01-ssh-hardening.sh)
Что делает скрипт:
- Создаёт пользователя
$ADMIN_USER(если ещё нет), добавляет в группуsudo. - Настраивает NOPASSWD-sudo (
/etc/sudoers.d/90-$ADMIN_USER+visudo -c) — обязательный анти-lockout шаг:useraddне задаёт пароль, и без NOPASSWD после отключения root на сервере не останется работающего sudo (боевой кейс 2026-07-09). Ключ — единственный фактор входа; безопасность не ниже заменяемой схемы «root по ключу». - Устанавливает
$SSH_KEY_PUBв/home/$ADMIN_USER/.ssh/authorized_keys(mode 600, owner — admin). - Правит
/etc/ssh/sshd_config:PermitRootLogin no,PasswordAuthentication no,Port $SSH_PORT. - Делает
sshd -t(syntax check). НЕ перезапускает sshd до подтверждения.
Запуск:
ssh root@$SERVER_IP "ADMIN_USER='$ADMIN_USER' SSH_KEY_PUB='$SSH_KEY_PUB' SSH_PORT='$SSH_PORT' bash -s" < scripts/01-ssh-hardening.shVerify (КРИТИЧНО — не перезапускай sshd до проверки!):
- В новом терминале:
ssh -p $SSH_PORT $ADMIN_USER@$SERVER_IP "id && sudo -n true && echo SUDO_OK"— обязаны сработать И вход, И беспарольный sudo. НетSUDO_OK→ это будущий lockout: НЕ перезапускай sshd, сначала разберись с sudoers. - Только после этого:
ssh root@$SERVER_IP "systemctl restart sshd". - Проверить, что root больше не пускает:
ssh root@$SERVER_IP "id"— должно отвалиться сPermission denied.
Шаг 2: UFW-setup (scripts/02-ufw-setup.sh)
Что делает: ставит ufw, default deny incoming + allow outgoing, открывает $SSH_PORT/tcp, 80/tcp, 443/tcp, активирует.
Запуск:
ssh $ADMIN_USER@$SERVER_IP -p $SSH_PORT "SSH_PORT='$SSH_PORT' sudo -E bash -s" < scripts/02-ufw-setup.shVerify: sudo ufw status verbose — Status: active; правила для $SSH_PORT/tcp, 80/tcp, 443/tcp ALLOW IN.
Шаг 3: fail2ban (scripts/03-fail2ban.sh)
Что делает: ставит fail2ban, создаёт /etc/fail2ban/jail.local с jail для sshd (maxretry=5, bantime=1h), включает.
Запуск:
ssh $ADMIN_USER@$SERVER_IP -p $SSH_PORT "SSH_PORT='$SSH_PORT' sudo -E bash -s" < scripts/03-fail2ban.shVerify: sudo fail2ban-client status sshd — Currently failed: 0, jail enabled.
Шаг 4: Docker (scripts/04-docker-install.sh)
Что делает: ставит Docker CE из официального APT-репозитория Docker, добавляет $ADMIN_USER в группу docker, ставит docker-compose-plugin.
Запуск:
ssh $ADMIN_USER@$SERVER_IP -p $SSH_PORT "ADMIN_USER='$ADMIN_USER' sudo -E bash -s" < scripts/04-docker-install.shVerify:
ssh $ADMIN_USER@$SERVER_IP -p $SSH_PORT "docker --version && docker compose version && docker run --rm hello-world"После добавления в группу docker оператору нужен relogin (или newgrp docker) — скрипт это печатает в конце.
Шаг 4.1: Canonical docker-сеть services (рекомендация, не блокер)
Зачем: прививает паттерн user-defined network с первой минуты. Без этого шага первый
же docker compose up без явной networks: секции попадёт на встроенный bridge (docker0),
где DNS по имени сервиса не работает. Эталон сетевой раскладки — .claude/knowledge/networking/_reference/server-networks-defaults.md
(§3 эталонная 4-сеть-модель, §4 decision tree).
Что делать:
ssh $ADMIN_USER@$SERVER_IP -p $SSH_PORT "docker network create services"Verify:
ssh $ADMIN_USER@$SERVER_IP -p $SSH_PORT "docker network ls | grep services"Должна появиться строка с services (драйвер bridge).
Опционально (когда понятно, что на сервере будут БД и наблюдаемость):
ssh $ADMIN_USER@$SERVER_IP -p $SSH_PORT "
docker network create --internal data &&
docker network create monitoring
"data сразу создаётся с --internal (запрет egress в интернет — защита БД даже при
компрометации приложения). Если БД нужен publish для psql с хоста — добавляется по
паттерну postgres-в-двух-сетях из эталона §3.3.
Это рекомендация, не блокер. Bootstrap корректно завершится и без 4.1 — но первый
deploy-service тогда столкнётся с необходимостью создать сеть руками. Когда оператор
ещё не знает, какой сервис будет первым, имеет смысл создать хотя бы services.
Шаг 5: Структура /opt + git-init (scripts/05-git-init.sh)
Что делает:
- Создаёт
/opt/{monitoring,admin-panel,shared-db,apps}(пустые папки). - Создаёт
$INFRA_DIR(по умолчанию/opt/infra),git init, ставит owner$ADMIN_USER. - Копирует
templates/gitignore-template→$INFRA_DIR/.gitignore. - Копирует
templates/pre-commit-gitleaks→$INFRA_DIR/.git/hooks/pre-commit(chmod +x). - Создаёт
$INFRA_DIR/decisions/0000-template.md,incidents/_template.md,runbooks/00-template.md,inventory/README.md(заглушки из проекта-носителя). - Делает первый commit:
chore: bootstrap (sha SSH=#, UFW=#, fail2ban=#, docker=#).
Запуск:
ssh $ADMIN_USER@$SERVER_IP -p $SSH_PORT "ADMIN_USER='$ADMIN_USER' INFRA_DIR='$INFRA_DIR' bash -s" < scripts/05-git-init.sh
# (templates перекидываются скриптом — он использует heredoc)Verify:
ssh $ADMIN_USER@$SERVER_IP -p $SSH_PORT "ls -la $INFRA_DIR && cd $INFRA_DIR && git log --oneline"Должны быть: .gitignore, decisions/, incidents/, runbooks/, inventory/, один первичный коммит.
Плюс gitleaks version отвечает (скрипт ставит git из apt и gitleaks бинарником с GitHub,
если их нет — минимальная Ubuntu 24.04 идёт без git, боевой кейс 2026-07-09).
Шаг 5.5: Приёмка — пройти чек-лист
Прежде чем объявлять сервер настроенным, прохожу
references/post-bootstrap-checklist.md —
проверка живым действием, а не по факту «скрипт отработал»: SSH пускает по ключу и НЕ
пускает root, firewall режет закрытые порты, fail2ban реально банит, Docker поднимает
контейнер, git-репозиторий инициализирован. Хотя бы один FAIL — bootstrap не закончен,
и это говорится оператору прямо, а не заминается.
Шаг 6: Дальнейшие шаги
После завершения этого скилла — рекомендуется (в этом порядке):
- Сначала —
/sysadmin-init. Создастagent-config.json(мозг: менеджер паролей)infra-config.json(карта: нужен ли мониторинг, куда складывать бэкапы, какой Telegram-бот). Без этого файлаsetup-backupsиinstall-monitoring-stackотказываются работать (STRICT-режим, см. их Шаг 0). Это 3-5 минут вопросов с обоснованиями вариантов.
- Затем
/setup-secrets-vault— настройка менеджера паролей. Если на шаге 1 ты уже указалsecrets.managerв конфиге, этот скилл пропустит интерактивный выбор и просто проверит установку. Без менеджера секреты будут оседать в.env-файлах в репо — это антипаттерн. - (Опционально)
/install-monitoring-stack— базовый стек мониторинга (Uptime Kuma + Beszel + Dozzle + Dockge + Diun + Telegram-алерты). - (Опционально)
/setup-backups— restic + offsite-хранилище (S3 / Backblaze B2 / WebDAV — Яндекс.Диск, NextCloud, ownCloud).
Почему именно в таком порядке: конфиг — паспорт агента под этого оператора. Все последующие скиллы читают его и подстраивают поведение. Без конфига каждый скилл вынужден был бы заново спрашивать «какой у тебя менеджер паролей», «нужны ли алерты» и т.д. — это alert fatigue.
Перед первым деплоем (/deploy-service): прочитать
.claude/knowledge/networking/_reference/server-networks-defaults.md — особенно §3
«эталонная 4-сеть-сегментация» и §4 «decision tree». Это эталон сетевой раскладки сервера
(какие сети создавать, что выставлять наружу, как nginx общается с контейнерами). Если на
шаге 4.1 уже создана сеть services — первый деплой пройдёт по дефолту-A раскладки.
Failed Attempts (граблекейс)
- «bootstrap → сразу setup-backups, без
/sysadmin-init» — оператор пропустил настройку конфига и попытался сразу настроить бэкапы.setup-backupsотказался работать (нет конфига инфрыinfra-config.json). Урок: после bootstrap первым идёт/sysadmin-init, потом всё остальное. Это документировано в Шаге 6 как порядок. - «Делал bootstrap зависимым от конфига» — конфига на свежем сервере не существует
по определению. Урок: bootstrap — OPTIONAL-режим чтения конфига (если есть локально
на машине оператора — подхватит
languageиoperator.timezone, иначе defaults). - «UFW работает в Docker» — UFW и Docker конфликтуют по iptables. Docker по умолчанию обходит UFW для контейнеров с
ports:. Решение: либоiptables=falseв/etc/docker/daemon.json(но тогда контейнеры теряют сеть), либо настройкаDOCKER-USERchain в UFW — см.references/ubuntu-vs-debian-quirks.md. На свежем сервере без контейнеров пока не критично, но знай заранее. Полная картина (3 решения по возрастанию радикальности + дефолт127.0.0.1:portбиндинг) — в эталоне.claude/knowledge/networking/_reference/server-networks-defaults.md§7. - «git есть на любом Ubuntu» — минимальные образы 24.04 идут БЕЗ git:
05-git-init.shпадал «git: command not found», а gitleaks в apt нет вовсе — hook-защита от секретов молча не появлялась. Урок (2026-07-09): скрипт 05 сам ставит git (apt) + gitleaks (бинарник с GitHub releases) и честно предупреждает, если GitHub недоступен. - «создал admin-юзера — sudo работает» —
useraddне задаёт пароль; с закомментированным NOPASSWD-блоком послеPermitRootLogin no+ restart sshd на сервере не остаётся работающего sudo (lockout, единственный путь — консоль провайдера). Урок (2026-07-09): NOPASSWD теперь обязательный шаг скрипта 01 + verifysudo -n trueДО рестарта sshd. - «
ufw limitна SSH — бесплатная защита» — нет, она дублирует fail2ban и мешает работе.ufw limitбанит IP при 6+ новых коннектах за 30 сек — это ловит не только брутфорс, но и ЛЕГИТИМНУЮ пачку SSH-подключений при автоматизации/деплое/миграции (боевой кейс 2026-07-09: прямой Mac→сервер рвалсяConnection closed/timed out, а fail2ban при этом был чист — виноват был UFW-лимит). Скилл ставитufw allow, а brute-force отдаёт fail2ban (шаг 03, jail sshd) — он банит по НЕУДАЧНЫМ логинам из auth.log, точечнее и без ложных срабатываний на частоту коннектов. Два слоя одной защиты не нужны. Диагностика на месте: симптомConnection closed by remote hostна рукопожатии = UFW-лимит или fail2ban;ufw status | grep 22покажетLIMITvsALLOW,fail2ban-client status sshd— реальные баны. - «acme.sh поставится с Docker» — лучше отдельным шагом после bootstrap, не входит в этот скилл. Делается вручную при первом TLS-сертификате (для каждого сервера домены индивидуальны).
- «SSH-порт 22 — небезопасно» — миф. fail2ban + ключи + отключённый PasswordAuthentication защищают; смена порта — security through obscurity. Меняем по желанию (меньше шума в логах), не по необходимости.
- «PermitRootLogin without-password — компромисс» — нет, отключай полностью (
no). Если корневой ключ скомпрометирован — последствия фатальны. У оператора естьsudoчерез$ADMIN_USER. - «fail2ban подхватит изменение SSH-порта сам» — нет, jail для sshd по умолчанию слушает port 22. Если сменил порт — поправь
/etc/fail2ban/jail.local. Скрипт03-fail2ban.shэто делает автоматически по$SSH_PORT.
Граничные случаи
- Уже установлен Docker → STOP, это НЕ свежий сервер. Используй
cleanup-existing-server. - Уже есть пользователь с именем
$ADMIN_USER→ подтвердить у оператора (использовать существующего или выбрать другое имя). Скрипт01детектит и спрашивает. - Selectel/Hetzner specifics → некоторые провайдеры блокируют
25/465/587SMTP-порты на egress (для борьбы со спамом) — для этого скилла неважно, но полезно знать. - Cloud-init не закончил инициализацию →
cloud-init statusдолжен бытьdone. Еслиrunning— подождать 1-2 минуты, иначеapt updateможет повиснуть на блокировке. - Двойная установка Docker (Docker от Ubuntu + Docker CE) → скрипт
04-docker-install.shделаетapt-get remove docker docker-engine docker.io containerd runcперед установкой Docker CE, чтобы не было конфликтов. - Сервер за NAT / без публичного IP → bootstrap всё равно работает, но UFW открывает порты для несуществующего внешнего трафика. Проверь с оператором, что VPN/проброс настроены.
- Не Ubuntu/Debian (CentOS, Alpine, Arch) → этот скилл не покрывает. Альпийский скрипт
02-ufw-setup.shотвалится — там нет ufw, естьiptablesнапрямую. Допиши под свою ОС или используй другой скилл.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

