Learning Platform
Глоссарий Troubleshooting
Урок 09.01 · 18 мин
Начальный
AuthBasic AuthAPI keysБезопасность

Basic Auth и API keys: где аутентификация начинается

В этом уроке мы рассмотрим самые простые способы аутентификации в HTTP API: Basic Auth (часть стандарта HTTP с 1996 года) и API keys (фактический стандарт у большинства публичных API). Эти два механизма — то, с чем Junior DE сталкивается ежедневно: 80% машинно-машинных интеграций ходят либо с Basic Auth, либо с API key в заголовке.

Главная мысль урока: оба механизма опасны при неправильной обработке (особенно Basic Auth — base64 это не шифрование), но при дисциплинированном использовании (HTTPS, ротация, секретное хранение) — это рабочий и удобный инструмент.


TLS handshake: как HTTPS защищает credentials

HTTP Basic Auth (RFC 7617): base64 это не шифрование

Basic Auth — старейший механизм аутентификации в HTTP, описан в RFC 7617 (актуализирован в 2015, оригинал из RFC 1945, 1996). Принцип примитивен: клиент кодирует пару username:password в base64 и кладёт в заголовок Authorization.

GET /api/users HTTP/1.1
Host: api.example.com
Authorization: Basic YWxpY2U6c2VjcmV0MTIz

YWxpY2U6c2VjcmV0MTIz — это base64 от строки alice:secret123. Декодировать в обратную сторону тривиально:

import base64

# Кодирование
credentials = "alice:secret123"
encoded = base64.b64encode(credentials.encode()).decode()
print(encoded)  # YWxpY2U6c2VjcmV0MTIz

# Декодирование (любой человек может это сделать с заголовком)
decoded = base64.b64decode("YWxpY2U6c2VjcmV0MTIz").decode()
print(decoded)  # alice:secret123

Ключевой факт: base64 — это кодирование, не шифрование. Любой, кто перехватит запрос (или прочитает в логе), за одну команду получает оригинальные username:password. Без HTTPS Basic Auth — это публичная отправка пароля.

Что защищает Basic Auth, а что нет
HTTPSTLS шифрует ВЕСЬ HTTP-запрос, включая заголовки. Перехватчик видит зашифрованный поток. Это и делает Basic Auth безопасным
Сетевой перехватБез TLS -- Wireshark на той же Wi-Fi сети, MITM на роутере, провайдер интернета -- все могут прочитать заголовок и декодировать base64
Логи серверовЕсли application пишет в лог Authorization-заголовок (а это часто происходит по умолчанию), пароли утекают в логи и могут быть прочитаны кем угодно с доступом к логам
Browser historyЕсли Basic Auth используется в URL вида https://user:[email protected]/, пароль попадает в историю, в bookmarks, в Referer заголовки
DANGER

Никогда не используйте Basic Auth по plain HTTP. Это эквивалентно отправке пароля открытым текстом. Любой инструмент сетевого мониторинга его прочитает.


Как браузер делает Basic Auth: 401 -> диалог -> повтор

В классическом сценарии Basic Auth сервер возвращает 401 без Authorization, заставляя клиента повторить запрос с правильным заголовком.

Basic Auth handshake
Client
Server
GET /api/users401 + WWW-Authenticate: Basic realm='API'GET /api/users + Authorization: Basic ...200 OK + JSON

В Python-клиенте мы обычно сразу шлём Authorization-заголовок без 401-handshake — это экономит один сетевой round-trip:

import requests
from requests.auth import HTTPBasicAuth

# Способ 1: через auth-параметр (рекомендуется)
response = requests.get(
    "https://api.example.com/users",
    auth=HTTPBasicAuth("alice", "secret123"),
)

# Способ 2: то же через tuple
response = requests.get(
    "https://api.example.com/users",
    auth=("alice", "secret123"),
)

# Способ 3: вручную (для понимания)
import base64
creds = base64.b64encode(b"alice:secret123").decode()
response = requests.get(
    "https://api.example.com/users",
    headers={"Authorization": f"Basic {creds}"},
)

Все три варианта приводят к идентичному HTTP-запросу. На практике первый — самый чистый.


API keys: «токен на каждый клиент»

API key — это случайно сгенерированный секретный токен, который сервер выдаёт клиенту (часто через личный кабинет на сайте API). Клиент шлёт ключ при каждом запросе, сервер по ключу определяет «какой клиент это и какие у него права».

В отличие от Basic Auth, API key:

  • Не предполагает логин/пароль пользователя — это про идентификацию клиента (приложения/сервиса), не человека
  • Один клиент может иметь несколько ключей (live/test, прод/стейдж, ключи для разных environments)
  • Легко отзывается: скомпрометированный ключ можно «обнулить» в одном месте, не меняя пароли пользователей

API keys — самый распространённый механизм для машинно-машинных интеграций. Stripe, OpenAI, SendGrid, Twilio, AWS — все начинаются с API key.


Где передавать API key: header vs query string vs body

Есть три места, куда обычно кладут API key. Разница принципиальна с точки зрения безопасности.

Где передавать API key
HTTP заголовокAuthorization: Bearer KEY или X-API-Key: KEY. Не попадает в логи URL, не попадает в Referer, не виден в browser history. Стандарт индустрии
Query string?api_key=KEY. Плохо: попадает в access logs Nginx/Apache, в Referer, в browser history, в системные мониторинги. Используется только legacy-системами
Request bodyPOST {api_key: KEY, ...}. Не попадает в логи (тело обычно не логируется), но не работает для GET-запросов. Редкая практика

Header — стандарт индустрии

Два самых распространённых формата заголовка:

# Вариант 1: Authorization: Bearer (унифицированный, RFC 6750)
Authorization: Bearer sk_live_abc123def456ghi789

# Вариант 2: X-API-Key (проще, но не стандартизирован)
X-API-Key: sk_live_abc123def456ghi789

Современные API (Stripe, OpenAI, SendGrid) используют формат Authorization: Bearer. Старые или внутренние API часто используют X-API-Key. В Python разница невелика:

import requests

# Вариант с Authorization
response = requests.get(
    "https://api.openai.com/v1/models",
    headers={"Authorization": "Bearer sk-proj-abc123..."},
)

# Вариант с X-API-Key
response = requests.get(
    "https://api.example.com/v1/data",
    headers={"X-API-Key": "abc123def456"},
)

Query string — опасно

# НЕ ДЕЛАЙТЕ ТАК
response = requests.get(
    f"https://api.example.com/v1/data?api_key={KEY}",
)

Почему опасно:

  • Access logs — Nginx/Apache по умолчанию логируют URL целиком. Ваш ключ оседает в файле /var/log/nginx/access.log на каждом промежуточном прокси
  • Referer header — если ответ содержит ссылку, по которой кликнут, новый сайт получит исходный URL в Referer
  • Browser history — если кто-то откроет такой URL в браузере, ключ сохранится в истории
  • CDN кэш — некоторые CDN кэшируют по полному URL, ключ попадает в кэш

Реальный инцидент: GitHub находил утёкшие API keys через простой поиск ?api_key= в публичных репозиториях логов и архивов. Десятки тысяч ключей утекли таким образом.

WARNING

Если API партнёра требует ключ в query string — это плохо спроектированный API. По возможности используйте header. Если выбора нет — постарайтесь хотя бы не логировать URL целиком на своей стороне и регулярнее ротируйте ключ.


Префиксы ключей: sk_live_, pk_test_ и зачем они

Современные API (Stripe пионер, остальные подтянулись) используют префиксы в API key, передающие метаданные. Например:

  • sk_live_... — secret key, production environment
  • sk_test_... — secret key, test/sandbox environment
  • pk_live_... — publishable key (можно класть во frontend), production
  • pk_test_... — publishable key, test
  • restk_... — restricted key (Stripe, ограниченные права)

Префиксы — это convention, не часть стандарта. Польза:

  • Сразу видно, какой environment (предотвращает «отправил тестовый webhook на прод»)
  • Сразу видно, можно ли публиковать (publishable key — да, secret — никогда)
  • Поиск утечек через regex по префиксу: GitHub Secret Scanning ищет sk_live_ в публичных репо и оповещает владельцев

Если делаете свой API — стоит копировать этот паттерн.


Когда использовать Basic Auth, а когда API keys

Оба механизма работают, но в разных контекстах.

Basic Auth vs API keys
Basic AuthЛогин/пароль человека. Подходит когда у API есть концепт пользователей с разными правами доступа. Реже используется для машинных интеграций
Use caseВнутренние admin-панели, простые corporate tools, тестовые системы, базовая защита HTTP-роутов
API keyМашинный токен, выдаётся приложению или сервису. Подходит для интеграций, где не нужен контекст конкретного пользователя
Use caseBackend-to-backend интеграции, ETL-пайплайны, мониторинг, любой machine-to-machine трафик

В работе Junior DE Basic Auth встречается реже, в основном для подключения к внутренним инструментам компании (admin panel, Grafana, прокси). API keys — постоянно: каждый external API, в который ходит ваш ETL, использует API key (или OAuth2, который мы рассмотрим в следующих уроках).


Ротация ключей: как и почему

Любой долгоживущий секрет рискует утечь: разработчик случайно закоммитил .env в публичный репо, лог утёк через bug-репорт, ноутбук с ключами в ~/.config/ потерян. Защита — ротация: периодическая смена ключей.

Цикл ротации:

Ротация API key -- без даунтайма
1. ГенерацияСоздаём новый ключ через UI/API провайдера. Старый ещё работает (overlap)
2. Update secretsОбновляем secrets manager (AWS Secrets Manager, Vault, GitHub Actions secrets). Деплоим приложения с новым ключом
3. VerifyПроверяем, что весь трафик идёт с новым ключом (по логам провайдера или собственным метрикам). Это критично -- иначе шаг 4 положит часть приложений
4. Revoke oldУдаляем/отзываем старый ключ. Если кто-то его пропустил, упадёт сразу -- лучше упасть в момент ротации, чем при инциденте

Частота ротации: индустриальный стандарт — 90 дней для секретов средней критичности, 30 дней для высокочувствительных. Многие провайдеры (AWS IAM, Vault) автоматизируют ротацию, многие — нет, и тогда это ваша задача (cron + runbook).

Аварийная ротация: если есть подозрение на утечку — ротируйте немедленно, без overlap. Лучше короткий даунтайм, чем взлом.


Хранение ключей: что, где, как

Ключи нельзя:

  • Коммитить в git (даже в private репо)
  • Логировать (даже фрагменты)
  • Шарить в Slack/email открытым текстом
  • Хранить в plain text на ноутбуке

Ключи можно/нужно:

  • Хранить в .env файле, добавленном в .gitignore
  • Использовать менеджер секретов (1Password, AWS Secrets Manager, HashiCorp Vault, Doppler)
  • В CI/CD — через secrets repository (GitHub Actions secrets, GitLab CI/CD variables)
  • На локальной машине — keychain (macOS Keychain, Windows Credential Manager, Linux Secret Service)

В Python типичная загрузка ключа из .env:

import os
from dotenv import load_dotenv
import requests

load_dotenv()  # подгружает .env файл
api_key = os.environ["STRIPE_API_KEY"]

response = requests.get(
    "https://api.stripe.com/v1/customers",
    headers={"Authorization": f"Bearer {api_key}"},
)

.env файл в проекте:

STRIPE_API_KEY=sk_live_abc123...
OPENAI_API_KEY=sk-proj-def456...

.gitignore:

.env
.env.local
.env.*.local
WARNING

Если случайно закоммитили ключ в git: ротируйте немедленно. git rm и rewrite history НЕ помогает — git rewrite видим в reflog, копии репозитория уже у других людей, история кешируется в GitHub. Ротация — единственный надёжный способ.


Сравнение: Basic Auth vs API key vs Bearer token

Чтобы держать в голове общую картину — таблица различий с теми механизмами, которые мы рассмотрим в следующих уроках.

Аутентификация: общая картина
Basic AuthЛогин:пароль в base64. Стандарт RFC 7617. Прост, но передаёт пароль с каждым запросом -- высокий риск компрометации в логах
API keyСлучайный токен, выданный системой. Без срока истечения (обычно). Простая ротация. Самый частый формат для машинных интеграций
Bearer token (JWT)Токен с подписью и метаданными. Срок жизни обычно короткий (15-60 минут). Изучаем в следующем уроке
OAuth2 access tokenBearer-токен, полученный через OAuth2 flow. Чаще короткоживущий + refresh token. Стандарт для делегированного доступа

В реальной работе DE использует все четыре, но в разных пропорциях: Basic Auth — изредка для внутренних tools, API keys — на каждом дне для внешних API, Bearer/JWT и OAuth2 — для интеграций с большими платформами (Google, Microsoft, GitHub).


Полный пример: интеграция с API через requests

Соберём practical-пример, который Junior DE напишет в первый день работы. Допустим, нужно забрать список customers из API через API key.

import os
import logging
from dotenv import load_dotenv
import requests

load_dotenv()
logger = logging.getLogger(__name__)


class APIClient:
    def __init__(self, api_key: str, base_url: str = "https://api.example.com/v1"):
        self.base_url = base_url
        self.session = requests.Session()
        # Заголовок применяется ко всем запросам через session
        self.session.headers["Authorization"] = f"Bearer {api_key}"
        # Нельзя логировать api_key -- даже в debug
        logger.info("APIClient initialized for %s", base_url)

    def list_customers(self, limit: int = 100) -> list[dict]:
        response = self.session.get(
            f"{self.base_url}/customers",
            params={"limit": limit},
            timeout=10,
        )
        response.raise_for_status()
        return response.json()["data"]


def main():
    api_key = os.environ["EXAMPLE_API_KEY"]
    client = APIClient(api_key=api_key)
    customers = client.list_customers(limit=50)
    print(f"Got {len(customers)} customers")


if __name__ == "__main__":
    main()

Что здесь критично:

  • Ключ читается из os.environ, не хардкод
  • Используется Session — заголовок устанавливается один раз, применяется ко всем запросам
  • В логе нигде не появляется api_key (только факт инициализации)
  • timeout=10 — защита от зависания
  • raise_for_status() — чтобы не молча обрабатывать 401/500

Итоги урока

Basic Auth — это base64-кодирование username:password в заголовке Authorization. Не шифрование: без HTTPS пароль виден всем. RFC 7617, для machine-to-machine редко используется, для admin tools — иногда.

API keys — случайные токены для идентификации клиента (приложения/сервиса). Передаются в заголовке Authorization: Bearer KEY или X-API-Key: KEY. Никогда не в query string — попадает в логи. Префиксы (sk_live_, pk_test_) помогают различать environments и упрощают поиск утечек.

Безопасное обращение с ключами: хранение в .env.gitignore) или secrets manager, ротация раз в 30-90 дней, аварийная ротация при подозрении на утечку. Никогда не коммитить, не логировать, не шарить открытым текстом.

В следующем уроке перейдём к Bearer-токенам и JWT — следующая ступень аутентификации, добавляющая срок жизни и метаданные в сам токен.


Проверка знанийKnowledge check
Junior нашёл в codebase коллеги такой код: requests.get(f'https://api.partner.com/data?api_key={KEY}&limit=100'). Какие три проблемы здесь, и как переписать правильно?
ОтветAnswer
Три проблемы: (1) API key в query string попадает в access logs Nginx/Apache на промежуточных прокси, в Referer заголовки исходящих ссылок, в CDN-кэши, в browser history. Высокий риск утечки. (2) Ключ передаётся через f-string в URL -- если KEY содержит спецсимволы (что бывает редко, но возможно), нужно URL-encoding. Лучше использовать params= в requests. (3) Нет timeout -- запрос может зависнуть навсегда, заблокировав поток. Правильный код: response = requests.get('https://api.partner.com/data', params={'limit': 100}, headers={'Authorization': f'Bearer {KEY}'}, timeout=10). Альтернатива -- X-API-Key заголовок, если так требует API. Главное -- KEY в header, не в URL.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 5. В заголовке Authorization: Basic YWxpY2U6c2VjcmV0 -- что внутри после 'Basic'?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 4