agentleFS
vefmvaivefmvai/sysadmin/.claude/skills/bootstrap-new-server/SKILL.md

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

The file

Files
SKILL.md
<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_PORT22SSH-порт (можно сменить на нестандартный — см. Failed Attempts)
ADMIN_USER(required)Имя пользователя для не-root-доступа (получит sudo)
SSH_KEY_PUB(required)Содержимое публичного ключа оператора
TIMEZONEUTCТаймзона сервера. UTC рекомендуется для серверов (логи коррелируются с другими сервисами без mental-конвертации); если оператор работает в одной локальной зоне и предпочитает её — переопределяется (например, Europe/Moscow, Europe/Berlin, America/New_York).
INFRA_DIR/opt/infraКуда инициализировать git-репозиторий
SERVER_HOSTautodetectИмя хоста (если не передано — берётся из 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)

Что делает скрипт:

  1. Создаёт пользователя $ADMIN_USER (если ещё нет), добавляет в группу sudo.
  2. Настраивает NOPASSWD-sudo (/etc/sudoers.d/90-$ADMIN_USER + visudo -c) — обязательный анти-lockout шаг: useradd не задаёт пароль, и без NOPASSWD после отключения root на сервере не останется работающего sudo (боевой кейс 2026-07-09). Ключ — единственный фактор входа; безопасность не ниже заменяемой схемы «root по ключу».
  3. Устанавливает $SSH_KEY_PUB в /home/$ADMIN_USER/.ssh/authorized_keys (mode 600, owner — admin).
  4. Правит /etc/ssh/sshd_config: PermitRootLogin no, PasswordAuthentication no, Port $SSH_PORT.
  5. Делает 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.sh

Verify (КРИТИЧНО — не перезапускай sshd до проверки!):

  1. В новом терминале: ssh -p $SSH_PORT $ADMIN_USER@$SERVER_IP "id && sudo -n true && echo SUDO_OK" — обязаны сработать И вход, И беспарольный sudo. Нет SUDO_OK → это будущий lockout: НЕ перезапускай sshd, сначала разберись с sudoers.
  2. Только после этого: ssh root@$SERVER_IP "systemctl restart sshd".
  3. Проверить, что 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.sh

Verify: 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.sh

Verify: 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.sh

Verify:

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)

Что делает:

  1. Создаёт /opt/{monitoring,admin-panel,shared-db,apps} (пустые папки).
  2. Создаёт $INFRA_DIR (по умолчанию /opt/infra), git init, ставит owner $ADMIN_USER.
  3. Копирует templates/gitignore-template → $INFRA_DIR/.gitignore.
  4. Копирует templates/pre-commit-gitleaks → $INFRA_DIR/.git/hooks/pre-commit (chmod +x).
  5. Создаёт $INFRA_DIR/decisions/0000-template.md, incidents/_template.md, runbooks/00-template.md, inventory/README.md (заглушки из проекта-носителя).
  6. Делает первый 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: Дальнейшие шаги

После завершения этого скилла — рекомендуется (в этом порядке):

  1. Сначала — /sysadmin-init. Создаст agent-config.json (мозг: менеджер паролей)
    • infra-config.json (карта: нужен ли мониторинг, куда складывать бэкапы, какой Telegram-бот). Без этого файла setup-backups и install-monitoring-stack отказываются работать (STRICT-режим, см. их Шаг 0). Это 3-5 минут вопросов с обоснованиями вариантов.
  2. Затем /setup-secrets-vault — настройка менеджера паролей. Если на шаге 1 ты уже указал secrets.manager в конфиге, этот скилл пропустит интерактивный выбор и просто проверит установку. Без менеджера секреты будут оседать в .env-файлах в репо — это антипаттерн.
  3. (Опционально) /install-monitoring-stack — базовый стек мониторинга (Uptime Kuma + Beszel + Dozzle + Dockge + Diun + Telegram-алерты).
  4. (Опционально) /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-USER chain в 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 + verify sudo -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 покажет LIMIT vs ALLOW, 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/587 SMTP-порты на 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 напрямую. Допиши под свою ОС или используй другой скилл.

Download now (ZIP)

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
What did you use it for? e.g. Reviewing pull requests in a Rails app

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.