Цели модуля

После модуля студент сможет:

  • Реализовывать HMAC с multi-key verification и replay protection
  • Проектировать стратегии ротации секретов без downtime
  • Применять принцип least privilege к сервисам и пользователям
  • Настраивать mTLS для internal communication
  • Понимать Zero Trust архитектуру
  • Обеспечивать безопасную внешнюю коммуникацию
  • Проектировать audit-friendly логирование
  • Понимать supply chain security (SBOM, sigstore)
  • Проводить базовый threat modeling

6.1 Security mindset для архитектора

Отличие Middle от Senior в security

Middle разработчик:

  • Знает OWASP Top 10
  • Применяет базовые защиты (параметризованные запросы, валидация)
  • Использует HMAC для webhook verification

Senior архитектор:

  • Проектирует security на уровне архитектуры
  • Думает о threat model до написания кода
  • Планирует ротацию секретов, key management
  • Обеспечивает defense in depth
  • Понимает compliance требования

Defense in depth

Многоуровневая защита: если один слой падает, другие защищают.

Уровень 1: Network (firewall, VPC, security groups)
Уровень 2: Transport (TLS, mTLS)
Уровень 3: Application (auth, validation, rate limiting)
Уровень 4: Data (encryption at rest, access control)
Уровень 5: Secrets (rotation, least privilege)
Уровень 6: Monitoring (audit logs, anomaly detection)

Каждый уровень независим. Компрометация одного не означает компрометацию всех.

Security by design

Security должен быть заложен в архитектуру с самого начала:

  1. Threat modeling до разработки
  2. Secure defaults (всё запрещено по умолчанию)
  3. Least privilege для всех компонентов
  4. Encryption everywhere (in transit, at rest)
  5. Audit logging для критичных операций
  6. Regular rotation для секретов
  7. Security testing в CI (SAST, DAST, SCA)

Compliance требования

Для разных доменов разные требования:

ДоменСтандартыТребования
ФинансыPCI DSSШифрование карточных данных, аудит
ЗдоровьеHIPAAЗащита медицинских данных, аудит
Персональные данныеGDPR, 152-ФЗПраво на удаление, согласие
ГоссекторГОСТ, ФСТЭКСертификация средств защиты

Архитектор должен понимать требования домена и закладывать их в архитектуру.


6.2 HMAC углублённо

Recap: базовый HMAC

HMAC (Hash-based Message Authentication Code) — подпись сообщения с использованием секретного ключа и хеш-функции.

mac = HMAC-SHA256(secret, message)

Проверка: expected = HMAC-SHA256(secret, message) if hmac.Equal(received, expected) { valid }

Multi-key verification

При ротации ключей нужен период когда принимаются оба ключа (текущий и предыдущий):

go
type MultiKeyVerifier struct {
    keys [][]byte  // [current, previous, ...]
}

func NewMultiKeyVerifier(keys ...[]byte) *MultiKeyVerifier {
    return &MultiKeyVerifier{keys: keys}
}

func (v *MultiKeyVerifier) Verify(message []byte, signature string) error {
    decoded, err := hex.DecodeString(signature)
    if err != nil {
        return fmt.Errorf("decode signature: %w", err)
    }

    for _, key := range v.keys {
        mac := hmac.New(sha256.New, key)
        mac.Write(message)
        expected := mac.Sum(nil)

        if hmac.Equal(decoded, expected) {
            return nil
        }
    }

    return errors.New("invalid signature")
}

Key identification

Для оптимизации можно добавлять key ID в подпись:

go
// Формат подписи: keyid.signature
// Пример: v2.a3f5b8c9d2e1...

func (v *MultiKeyVerifier) VerifyWithKeyID(message []byte, signature string) error {
    parts := strings.SplitN(signature, ".", 2)
    if len(parts) != 2 {
        return errors.New("invalid signature format")
    }

    keyID := parts[0]
    sig := parts[1]

    key, err := v.getKeyByID(keyID)
    if err != nil {
        return fmt.Errorf("unknown key: %w", err)
    }

    return v.verifyWithKey(message, sig, key)
}

Это позволяет не перебирать все ключи, а сразу использовать нужный.

Replay protection

Злоумышленник может перехватить валидный запрос и повторить. Защита: timestamp + nonce.

Формат подписываемого сообщения:

message = webhook_id + "." + timestamp + "." + body

signature = HMAC-SHA256(secret, message)

Verification:

go
func (v *Verifier) VerifyWebhook(
    body []byte,
    webhookID string,
    timestamp int64,
    signature string,
) error {
    // 1. Проверяем timestamp (не старше 5 минут)
    now := time.Now().Unix()
    if now - timestamp > 300 {
        return errors.New("timestamp too old")
    }
    if timestamp - now > 300 {
        return errors.New("timestamp in future")
    }

    // 2. Проверяем nonce (webhook ID не использовался)
    if v.nonceStore.Exists(webhookID) {
        return errors.New("webhook already processed")
    }

    // 3. Проверяем подпись
    message := fmt.Sprintf("%s.%d.%s", webhookID, timestamp, body)
    if err := v.Verify([]byte(message), signature); err != nil {
        return err
    }

    // 4. Запоминаем nonce
    v.nonceStore.Add(webhookID, time.Unix(timestamp, 0))

    return nil
}

Nonce store

Для хранения использованных nonce:

go
type NonceStore struct {
    mu     sync.Mutex
    nonces map[string]time.Time
    ttl    time.Duration
}

func NewNonceStore(ttl time.Duration) *NonceStore {
    return &NonceStore{
        nonces: make(map[string]time.Time),
        ttl:    ttl,
    }
}

func (s *NonceStore) Exists(nonce string) bool {
    s.mu.Lock()
    defer s.mu.Unlock()
    _, ok := s.nonces[nonce]
    return ok
}

func (s *NonceStore) Add(nonce string, ts time.Time) {
    s.mu.Lock()
    defer s.mu.Unlock()
    s.nonces[nonce] = ts
}

// Периодическая очистка старых nonce
func (s *NonceStore) Cleanup() {
    s.mu.Lock()
    defer s.mu.Unlock()
    cutoff := time.Now().Add(-s.ttl)
    for nonce, ts := range s.nonces {
        if ts.Before(cutoff) {
            delete(s.nonces, nonce)
        }
    }
}

Для распределённых систем используйте Redis вместо in-memory:

go
func (s *RedisNonceStore) Add(ctx context.Context, nonce string, ttl time.Duration) (bool, error) {
    // SETNX: устанавливает только если ключ не существует
    return s.redis.SetNX(ctx, "nonce:"+nonce, "1", ttl).Result()
}

Timing-safe comparison

ВСЕГДА используйте hmac.Equal для сравнения подписей:

go
// ПЛОХО: уязвимо к timing attacks
if signature == expected { ... }

// ПРАВИЛЬНО: constant-time comparison
if hmac.Equal(decoded, expected) { ... }

Timing attack: атакующий измеряет время сравнения и посимвольно угадывает подпись. hmac.Equal всегда выполняется за одинаковое время.


6.3 Secret rotation strategies

Зачем нужна ротация

Секреты могут быть скомпрометированы:

  • Утечка через логи или ошибки в коде
  • Доступ бывшего сотрудника
  • Взлом секрет-менеджера
  • Компрометация backup
  • Insider threat

Регулярная ротация ограничивает окно экспозиции.

Периодичность ротации

СекретПериодичностьПричина
HMAC ключи для webhook30-90 днейВнешние интеграции
API ключи90 днейСтандарт индустрии
Пароли БД90 днейCompliance
TLS сертификаты90 днейLet's Encrypt
Service account tokens24 часаShort-lived
Encryption keys1 годДорого ротировать

Двухфазная ротация

Фаза 1: Добавление нового ключа

Время T:
- current_key = key_v1
- Генерируем key_v2
- Сохраняем: current=key_v2, previous=key_v1
- Verifier принимает оба ключа
- Signer подписывает только key_v2

Фаза 2: Удаление старого ключа

Время T + 24h (после миграции всех клиентов):
- current_key = key_v2
- previous_key = null
- Verifier принимает только key_v2

Реализация ротации

go
type SecretRotator struct {
    provider repository.SecretProvider
    notifier Notifier
    logger   *slog.Logger
}

func (r *SecretRotator) RotateHMACKey(ctx context.Context) error {
    // 1. Получаем текущий ключ и метаданные
    current, err := r.provider.GetSecret(ctx, "hmac-key")
    if err != nil {
        return fmt.Errorf("get current key: %w", err)
    }

    metadata, err := r.provider.GetMetadata(ctx, "hmac-key")
    if err != nil {
        return fmt.Errorf("get metadata: %w", err)
    }

    // Проверяем что пора ротировать
    if time.Since(metadata.CreatedAt) < RotationPeriod {
        r.logger.Info("rotation not due yet",
            "created_at", metadata.CreatedAt,
            "next_rotation", metadata.CreatedAt.Add(RotationPeriod),
        )
        return nil
    }

    // 2. Генерируем новый ключ
    newKey, err := generateSecureKey(64)
    if err != nil {
        return fmt.Errorf("generate key: %w", err)
    }

    // 3. Сохраняем с previous key
    if err := r.provider.Rotate(ctx, "hmac-key", newKey, current.Value); err != nil {
        return fmt.Errorf("rotate key: %w", err)
    }

    // 4. Уведомляем команду
    r.notifier.Notify(ctx, Notification{
        Title:   "HMAC key rotated",
        Message: "New key active. Previous key valid for 24h.",
        Severity: SeverityInfo,
    })

    r.logger.Info("hmac key rotated",
        "key_id", newKey.ID,
        "previous_key_id", current.ID,
    )

    return nil
}

Автоматическая ротация через cron job

В шаблоне проекта есть hmac-rotator cron job:

go
// internal/handlers/jobs/hmac_rotator/handler.go
func (h *Handler) Run(ctx context.Context) error {
    return h.rotator.RotateHMACKey(ctx)
}

// Расписание: каждый день в 3:00
func (h *Handler) Schedule() string {
    return "0 0 3 * * *"
}

Ротация TLS сертификатов

Для TLS используем Let's Encrypt с автоматической ротацией:

go
// certbot автоматически обновляет сертификаты каждые 60 дней
// cron job: certbot renew

Для Kubernetes:

  • cert-manager автоматически управляет сертификатами
  • Автообновление за 30 дней до истечения

Ротация паролей БД

Для managed БД (Yandex, AWS RDS) используйте встроенную ротацию:

go
// Yandex Lockbox: автоматическая ротация пароля БД
// AWS Secrets Manager: автоматическая ротация через Lambda

Для self-hosted PostgreSQL:

1. Создаём нового пользователя с новым паролем
2. Обновляем конфиг сервиса
3. Перезапускаем сервис (rolling restart)
4. Удаляем старого пользователя

6.4 Least privilege для сервисов

Принцип least privilege

Каждый компонент получает минимально необходимые права:

  • Не больше чем нужно для работы
  • Не меньше чем нужно для работы

Least privilege для сервисных аккаунтов

Пример для Yandex Cloud:

hcl
// ПЛОХО: сервис с ролью admin
resource "yandex_iam_service_account" "app" {
  name = "app-sa"
}

resource "yandex_resourcemanager_folder_iam_member" "app_admin" {
  folder_id = var.folder_id
  role      = "admin"  // слишком много прав!
  member    = "serviceAccount:${yandex_iam_service_account.app.id}"
}

// ПРАВИЛЬНО: минимальные роли
resource "yandex_iam_service_account" "app" {
  name = "app-sa"
}

// Только чтение секретов
resource "yandex_lockbox_secret_iam_binding" "app_viewer" {
  secret_id = yandex_lockbox_secret.app_secrets.id
  role      = "lockbox.payloadViewer"
  members   = ["serviceAccount:${yandex_iam_service_account.app.id}"]
}

// Только запись в бакет
resource "yandex_storage_bucket_iam_binding" "app_writer" {
  bucket = yandex_storage_bucket.uploads.bucket
  role   = "storage.writer"
  members = ["serviceAccount:${yandex_iam_service_account.app.id}"]
}

Least privilege для пользователей БД

sql
// ПЛОХО: superuser для приложения
CREATE USER app WITH SUPERUSER PASSWORD 'secret';

// ПРАВИЛЬНО: минимальные права
CREATE USER app WITH PASSWORD 'secret';
GRANT CONNECT ON DATABASE mydb TO app;
GRANT USAGE ON SCHEMA public TO app;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO app;
GRANT USAGE ON ALL SEQUENCES IN SCHEMA public TO app;

// НЕ даём:
// - CREATE, DROP, ALTER
// - Доступ к pg_catalog для записи
// - SUPERUSER, CREATEDB, CREATEROLE

Least privilege для API

Разные роли — разные права:

go
type Role string

const (
    RoleAdmin  Role = "admin"
    RoleUser   Role = "user"
    RoleViewer Role = "viewer"
)

var rolePermissions = map[Role]map[string]bool{
    RoleAdmin: {
        "repos:create": true,
        "repos:read":   true,
        "repos:update": true,
        "repos:delete": true,
        "users:manage": true,
    },
    RoleUser: {
        "repos:create": true,
        "repos:read":   true,
        "repos:update": true,
        "repos:delete": false,  // не может удалять
        "users:manage": false,
    },
    RoleViewer: {
        "repos:create": false,
        "repos:read":   true,   // только чтение
        "repos:update": false,
        "repos:delete": false,
        "users:manage": false,
    },
}

Middleware для проверки прав:

go
func RequirePermission(permission string) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            user := GetUser(r.Context())
            if !hasPermission(user.Role, permission) {
                writeProblem(w, http.StatusForbidden, "Forbidden", "insufficient permissions")
                return
            }
            next.ServeHTTP(w, r)
        })
    }
}

Least privilege для сети

Изоляция сервисов на уровне сети:

hcl
// Security groups в Yandex Cloud / AWS
// Приложение принимает трафик только от load balancer
resource "yandex_vpc_security_group_rule" "app_ingress" {
  direction     = "ingress"
  protocol      = "TCP"
  port          = 8080
  source_security_group_id = yandex_vpc_security_group.lb.id
}

// БД принимает трафик только от приложения
resource "yandex_vpc_security_group_rule" "db_ingress" {
  direction     = "ingress"
  protocol      = "TCP"
  port          = 5432
  source_security_group_id = yandex_vpc_security_group.app.id
}

6.5 mTLS и Zero Trust

Что такое mTLS

Mutual TLS — обе стороны предъявляют сертификаты:

  • Клиент проверяет сертификат сервера (как обычный TLS)
  • Сервер проверяет сертификат клиента (дополнительно)

Это обеспечивает аутентификацию на транспортном уровне.

Когда использовать mTLS

  • Internal communication между микросервисами
  • Подключение к БД
  • Внешние API с высоким уровнем безопасности
  • Zero Trust архитектура

Настройка mTLS в Go

Сервер:

go
tlsConfig := &tls.Config{
    MinVersion: tls.VersionTLS12,
    ClientAuth: tls.RequireAndVerifyClientCert,
    ClientCAs:  clientCAPool,  // CA для проверки клиентских сертификатов
}

srv := &http.Server{
    Addr:      ":8443",
    Handler:   router,
    TLSConfig: tlsConfig,
}

srv.ListenAndServeTLS("server.crt", "server.key")

Клиент:

go
cert, err := tls.LoadX509KeyPair("client.crt", "client.key")
if err != nil {
    return fmt.Errorf("load client cert: %w", err)
}

tlsConfig := &tls.Config{
    Certificates: []tls.Certificate{cert},
    RootCAs:      serverCAPool,  // CA для проверки сервера
    MinVersion:   tls.VersionTLS12,
}

client := &http.Client{
    Transport: &http.Transport{
        TLSClientConfig: tlsConfig,
    },
}

Zero Trust архитектура

Принцип: "Never trust, always verify."

В традиционной архитектуре:

  • Внутри сети всё доверенно
  • Периметр защищает от внешних угроз

В Zero Trust:

  • Нет доверенной сети
  • Каждый запрос аутентифицируется и авторизуется
  • Шифрование везде (even internal)

Компоненты Zero Trust

  1. Identity: аутентификация каждого запроса

    • Service-to-service: mTLS, JWT
    • User-to-service: OAuth 2.0, OIDC
  2. Authorization: проверка прав для каждого запроса

    • RBAC, ABAC
    • Policy engine (OPA, Casbin)
  3. Encryption: шифрование всего трафика

    • TLS для внешнего
    • mTLS для внутреннего
  4. Microsegmentation: изоляция сервисов

    • Network policies
    • Service mesh (Istio, Linkerd)

Service mesh для mTLS

Для Kubernetes service mesh автоматизирует mTLS:

Istio:

  • Автоматическая ротация сертификатов
  • mTLS между всеми подами
  • Traffic policies
  • Observability

Пример PeerAuthentication в Istio:

yaml
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
  name: default
  namespace: production
spec:
  mtls:
    mode: STRICT  # Требовать mTLS для всех

6.6 Secure external communication

HTTPS везде

Все внешние коммуникации только через HTTPS:

go
// ПЛОХО: HTTP
client.Get("http://api.example.com")

// ПРАВИЛЬНО: HTTPS
client.Get("https://api.example.com")

TLS конфигурация

Минимальная версия TLS 1.2:

go
tlsConfig := &tls.Config{
    MinVersion: tls.VersionTLS12,
    // Запрет слабых cipher suites
    CipherSuites: []uint16{
        tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
        tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
        tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
        tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
    },
}

Certificate pinning (осторожно)

Для критичных интеграций можно pin'ить сертификаты:

go
// ПИННинг публичного ключа
expectedPin := "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="

// Проверяем при handshake
tlsConfig := &tls.Config{
    VerifyPeerCertificate: func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
        for _, rawCert := range rawCerts {
            cert, _ := x509.ParseCertificate(rawCert)
            pin := hashPublicKey(cert.RawSubjectPublicKeyInfo)
            if pin == expectedPin {
                return nil
            }
        }
        return errors.New("certificate pin mismatch")
    },
}

ОСТОРОЖНО: pinning ломается при ротации сертификатов. Используйте только для критичных интеграций с backup pins.

Валидация сертификатов

Никогда не отключайте валидацию сертификатов:

go
// ПЛОХО: отключение валидации
tlsConfig := &tls.Config{
    InsecureSkipVerify: true,  // НЕ ДЕЛАЙТЕ ТАК!
}

// ПРАВИЛЬНО: валидация с custom CA если нужно
certPool := x509.NewCertPool()
certPool.AppendCertsFromPEM(caCert)

tlsConfig := &tls.Config{
    RootCAs: certPool,
}

Timeout для внешних вызовов

Всегда устанавливайте таймауты:

go
client := &http.Client{
    Timeout: 30 * time.Second,
    Transport: &http.Transport{
        TLSHandshakeTimeout:   10 * time.Second,
        ResponseHeaderTimeout: 10 * time.Second,
        IdleConnTimeout:       90 * time.Second,
    },
}

6.7 Audit logging углублённо

Что такое audit log

Audit log — журнал критичных операций для:

  • Расследования инцидентов
  • Compliance (PCI DSS, HIPAA, GDPR)
  • Отслеживания действий пользователей
  • Обнаружения аномалий

Что логировать в audit

Обязательные события:

  • Аутентификация (успех/неудача)
  • Авторизация (доступ к ресурсам)
  • Создание/изменение/удаление данных
  • Изменение прав и ролей
  • Доступ к секретам
  • Административные действия

Структура audit event

go
type AuditEvent struct {
    Timestamp  time.Time             `json:"timestamp"`
    EventID    string                `json:"event_id"`
    UserID     string                `json:"user_id"`
    UserAgent  string                `json:"user_agent"`
    IPAddress  string                `json:"ip_address"`
    Action     string                `json:"action"`       // "repo.delete"
    Resource   string                `json:"resource"`     // "repo-123"
    ResourceType string              `json:"resource_type"` // "repo"
    Result     string                `json:"result"`       // "success", "failure"
    Error      string                `json:"error,omitempty"`
    Metadata   map[string]interface{} `json:"metadata,omitempty"`
    RequestID  string                `json:"request_id"`
    TraceID    string                `json:"trace_id"`
}

Реализация audit logger

go
type AuditLogger struct {
    logger *slog.Logger
    // Отдельный writer для audit (не смешивать с обычными логами)
    writer io.Writer
}

func NewAuditLogger(writer io.Writer) *AuditLogger {
    return &AuditLogger{
        logger: slog.New(slog.NewJSONHandler(writer, nil)),
        writer: writer,
    }
}

func (l *AuditLogger) Log(ctx context.Context, event AuditEvent) {
    event.Timestamp = time.Now()
    event.EventID = generateID()
    event.RequestID = middleware.GetRequestID(ctx)
    event.TraceID = tracing.GetTraceID(ctx)

    l.logger.Info("audit",
        "event_id", event.EventID,
        "user_id", event.UserID,
        "action", event.Action,
        "resource", event.Resource,
        "result", event.Result,
        "ip", event.IPAddress,
    )
}

Использование в сервисах

go
func (s *RepoService) DeleteRepo(ctx context.Context, id string) error {
    userID := middleware.GetUserID(ctx)

    err := s.repo.Delete(ctx, id)

    // Audit log независимо от результата
    result := "success"
    errMsg := ""
    if err != nil {
        result = "failure"
        errMsg = err.Error()
    }

    s.auditLogger.Log(ctx, AuditEvent{
        UserID:       userID,
        Action:       "repo.delete",
        Resource:     id,
        ResourceType: "repo",
        Result:       result,
        Error:        errMsg,
        IPAddress:    middleware.GetIPAddress(ctx),
        UserAgent:    middleware.GetUserAgent(ctx),
    })

    return err
}

Хранение audit logs

Требования:

  • Append-only (нельзя изменить или удалить)
  • Долгий retention (1-7 лет в зависимости от compliance)
  • Защита от несанкционированного доступа
  • Отдельное хранилище от приложения

Варианты:

  • Отдельная БД с restricted access
  • Object storage с versioning (S3, Yandex Object Storage)
  • Специализированные сервисы (CloudTrail, Yandex Audit Logs)

Неизменяемость audit logs

sql
// Append-only таблица
CREATE TABLE audit_events (
    id UUID PRIMARY KEY,
    timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    event JSONB NOT NULL
);

// Запрет UPDATE и DELETE
REVOKE UPDATE, DELETE ON audit_events FROM app;

// Или используем partitioning по времени
// и удаляем только старые partitions

6.8 Supply chain security

Что такое supply chain attack

Атака через зависимости и инструменты сборки:

  • Вредоносный пакет в registry
  • Компрометированный CI/CD
  • Подмена бинарника при деплое

Примеры:

  • event-stream (npm, 2018)
  • ua-parser-js (npm, 2021)
  • SolarWinds (2020)

SBOM (Software Bill of Materials)

SBOM — список всех компонентов приложения:

  • Прямые зависимости
  • Транзитивные зависимости
  • Версии
  • Лицензии

Генерация SBOM в Go:

# Syft - генерация SBOM
syft packages dir:. -o json > sbom.json

# Или для Docker образа
syft packages myapp:latest -o spdx-json > sbom.json

Проверка зависимостей

# govulncheck - известные уязвимости в Go коде
govulncheck ./...

# gosec - статический анализ безопасности
gosec ./...

# Trivy - сканирование зависимостей
trivy fs --security-checks vuln ./

# npm audit, pip audit для других экосистем

Подпись артефактов

Используем sigstore/cosign для подписи Docker образов:

# Подпись образа
cosign sign --key cosign.key registry.example.com/myapp:v1.0.0

# Проверка подписи
cosign verify --key cosign.pub registry.example.com/myapp:v1.0.0

Проверка checksum

Для бинарных артефактов:

# При сборке генерируем checksum
sha256sum git-server > git-server.sha256

# При деплое проверяем
sha256sum -c git-server.sha256

Reproducible builds

Сборка должна быть воспроизводимой:

dockerfile
# Фиксированные версии в go.mod
# go.sum для проверки зависимостей
# Фиксированные версии базовых образов в Dockerfile
# Build args для версионирования

FROM golang:1.23.4-alpine3.19 AS builder
# Не FROM golang:latest

CI/CD security

  1. Secrets management в CI

    • Не хардкодить секреты в pipeline
    • Использовать secret storage (GitHub Secrets, Vault)
  2. Least privilege для CI

    • Минимальные права для pipeline
    • Read-only где возможно
  3. Проверка кода перед merge

    • SAST (gosec, Semgrep)
    • SCA (Trivy, govulncheck)
    • Code review
  4. Подпись коммитов

    • GPG подписи для коммитов
    • Verified commits в git

6.9 Threat modeling

Что такое threat modeling

Процесс идентификации угроз и определения контрмер до написания кода.

STRIDE методология

КатегорияУгрозаПример
SpoofingПодмена личностиФальшивый JWT токен
TamperingИзменение данныхSQL injection
RepudiationОтказ от действийНет audit log
Information DisclosureУтечка данныхPII в логах
Denial of ServiceОтказ в обслуживанииНет rate limiting
Elevation of PrivilegeПовышение правIDOR уязвимость

Пример threat modeling для API

Шаг 1: Определяем компоненты

Client -> Load Balancer -> API Gateway -> Service -> Database
                                   -> Cache
                                   -> External API

Шаг 2: Идентифицируем угрозы по STRIDE

Spoofing:
- Подмена JWT токена -> Валидация подписи, expiration
- Подмена IP -> Не доверять X-Forwarded-For без проверки

Tampering:
- SQL injection -> Параметризованные запросы
- Изменение запроса в transit -> HTTPS

Information Disclosure:
- PII в логах -> Маскирование
- Ошибки с деталями -> Generic error messages

Denial of Service:
- Много запросов -> Rate limiting
- Большие payload -> MaxBytesReader

Шаг 3: Определяем контрмеры

Для каждой угрозы:
- Контрмера
- Приоритет (High/Medium/Low)
- Ответственный
- Дедлайн

DREAD для оценки рисков

ФакторОписаниеОценка 1-5
DamageУщерб от эксплуатации
ReproducibilityЛегко воспроизвести
ExploitabilityЛегко эксплуатировать
Affected usersСколько пользователей затронуто
DiscoverabilityЛегко обнаружить

Risk = среднее значение. High: > 4, Medium: 2-4, Low: < 2.

Практический threat modeling

Проводите threat modeling:

  • При проектировании нового сервиса
  • При добавлении новой функциональности
  • При изменении архитектуры
  • Перед публичным релизом

Результат: документ с угрозами и контрмерами, приоритизированный backlog security задач.


Привязка к шаблону проекта

Где security в шаблоне

internal/security/
├── hmac/
│   ├── signer.go           # HMAC signing
│   ├── verifier.go         # HMAC verification
│   └── rotation.go         # Ротация ключей
└── policy/
    └── labels.go           # Security labels

internal/handlers/jobs/hmac_rotator/
└── handler.go              # HMAC rotator cron job

internal/adapters/lockbox/
└── secret_provider.go      # Lockbox secrets

Что изучаем на примере шаблона

  1. HMAC signer и verifier с multi-key support
  2. Ротация ключей через cron job
  3. Lockbox для секретов
  4. Security labels для классификации данных
  5. PII masking в логах

Задание: изучите internal/security/

  • Как реализован HMAC?
  • Есть ли multi-key support?
  • Как настроена ротация?
  • Какие security policies?

💱 Компромиссы

  • Жёсткий один ключ vs окно двухфазной ротации: удалив previous ключ сразу, вы закрываете окно атаки, но ломаете клиентов, ещё не перешедших на новый; 24 часа с двумя ключами дают ротацию без downtime, но скомпрометированный старый ключ тоже продолжает работать.
  • In-memory nonce store vs Redis: локальный store быстр и не тянет зависимостей, но в распределённой системе replay пройдёт на другом инстансе; Redis с SETNX закрывает все инстансы сразу, но это сетевой хоп на каждый webhook и ещё одна точка отказа.
  • Least privilege vs широкие роли: минимальные гранты вроде lockbox.payloadViewer и SELECT/INSERT ограничивают blast radius, но каждое новое действие требует правки ролей; admin-роль не мешает работе, но компрометация одного сервиса раскрывает всё.
  • mTLS на каждый хоп vs доверенная сеть: mutual TLS аутентифицирует обе стороны по Zero Trust, но сертификаты надо раздавать и ротировать — вручную это боль, нужен cert-manager или Istio; доверие внутри периметра проще, но взлом одного pod'а отдаёт всю сеть.
  • Certificate pinning vs обычная валидация: pin публичного ключа защищает даже при компрометации CA, но ломается при ротации сертификата, если не заложены backup pins; стандартная проверка по RootCAs переживает любую ротацию, но доверяет каждому CA из пула.

Практическое задание

Задание 1: Multi-key HMAC verifier

Реализуйте verifier с поддержкой нескольких ключей:

go
type MultiKeyVerifier struct {
    keys map[string][]byte  // keyID -> key
}

func (v *MultiKeyVerifier) Verify(message []byte, signature string) error

Поддержка формата: keyid.signature

Задание 2: Replay protection

Добавьте replay protection для webhook:

go
func (v *Verifier) VerifyWebhook(body []byte, webhookID string, timestamp int64, signature string) error

С:

  • Проверкой timestamp (не старше 5 минут)
  • Nonce store для webhook ID
  • Cleanup старых nonce

Задание 3: Secret rotation

Реализуйте ротацию HMAC ключей:

go
func (r *SecretRotator) RotateHMACKey(ctx context.Context) error

С:

  • Генерацией нового ключа
  • Сохранением previous key
  • Уведомлением команды
  • Cron job для автоматической ротации

Задание 4: Least privilege для БД

Создайте пользователя БД с минимальными правами:

sql
CREATE USER app WITH PASSWORD 'secret';
GRANT CONNECT ON DATABASE mydb TO app;
GRANT USAGE ON SCHEMA public TO app;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO app;

Проверьте что пользователь не может:

  • Создавать таблицы
  • Удалять таблицы
  • Изменять схему

Задание 5: mTLS

Настройте mTLS между двумя сервисами:

Сервер:

  • RequireAndVerifyClientCert
  • ClientCAs с CA для клиентских сертификатов

Клиент:

  • Клиентский сертификат
  • RootCAs с CA сервера

Протестируйте что запрос без клиентского сертификата отклоняется.

Задание 6: Audit logging

Реализуйте audit logger:

go
func (l *AuditLogger) Log(ctx context.Context, event AuditEvent)

Добавьте audit logging для:

  • Создания репозитория
  • Удаления репозитория
  • Изменения прав пользователя
  • Доступа к секретам

Задание 7: SBOM

Сгенерируйте SBOM для вашего проекта:

syft packages dir:. -o json > sbom.json

Просканируйте на уязвимости:

trivy fs --security-checks vuln ./

Исправьте найденные критичные уязвимости.

Задание 8: Threat modeling

Проведите threat modeling для вашего сервиса по STRIDE:

  1. Нарисуйте диаграмму компонентов
  2. Идентифицируйте угрозы по каждой категории STRIDE
  3. Определите контрмеры
  4. Приоритизируйте по DREAD
  5. Создайте backlog security задач

Quiz "Проверь себя"

1. Что такое принцип least privilege?
2. Какие категории угроз выделяет STRIDE? — выберите все подходящие
3. Что такое defense in depth?
4. Зачем HMAC-verifier поддерживает несколько ключей?
5. Что такое SBOM?
6. Какие требования к audit log? — выберите все подходящие
7. Какой функцией корректно сравнивать HMAC-подписи в Go?
go
expected := computeMAC(message, key)
received, _ := hex.DecodeString(sigHex)
// сравнение подписей:
8. Каковы принципы Zero Trust? — выберите все подходящие
9. Как защитить webhook от replay attacks? — выберите все подходящие
10. Что такое mTLS и где он применяется?

✅ Итоги модуля

Студент должен:

  • Реализовывать HMAC с multi-key и replay protection
  • Проектировать ротацию секретов без downtime
  • Применять least privilege на всех уровнях
  • Настраивать mTLS для internal communication
  • Понимать Zero Trust архитектуру
  • Обеспечивать безопасную внешнюю коммуникацию
  • Проектировать audit-friendly логирование
  • Понимать supply chain security
  • Проводить базовый threat modeling по STRIDE
  • Ответить на все вопросы Quiz

🔗 Мостик к Модулю 7

Следующий модуль: Модуль 7. Storage and Data Strategy - schema evolution, read/write separation, backups and restore, NFS vs S3, data retention, disaster recovery strategy. Привязка к internal/adapters/nfs/, internal/adapters/s3/, internal/dr/ шаблона.