Цели модуля
После модуля студент сможет:
- Применять OWASP Top 10 на практике в Go-сервисах
- Различать authentication и authorization
- Реализовывать HMAC signing и verification
- Безопасно управлять секретами и их ротацией
- Защищаться от SQL injection, XSS, CSRF
- Правильно логировать без утечки PII
- Настраивать HTTPS и security headers
- Реализовывать rate limiting
9.1 OWASP Top 10 на практике
Что такое OWASP Top 10
OWASP (Open Web Application Security Project) публикует список 10 самых критичных уязвимостей web-приложений. Знание этих уязвимостей обязательно для backend-разработчика.
Top 10 уязвимостей (2021)
| № | Уязвимость | Защита в Go |
|---|---|---|
| A01 | Broken Access Control | RBAC, проверки прав в middleware |
| A02 | Cryptographic Failures | TLS, secure hashing, не хранить пароли |
| A03 | Injection | Параметризованные запросы, валидация |
| A04 | Insecure Design | Threat modeling, secure patterns |
| A05 | Security Misconfiguration | Минимальные права, дефолтные пароли |
| A06 | Vulnerable Components | go mod tidy, govulncheck, Trivy |
| A07 | Auth Failures | JWT validation, rate limiting |
| A08 | Software and Data Integrity | SBOM, sigstore, проверка checksum |
| A09 | Logging Failures | PII masking, audit logs |
| A10 | SSRF | Валидация URL, allowlist |
Как проверять безопасность в CI
bash# govulncheck - проверка известных уязвимостей go install golang.org/x/vuln/cmd/govulncheck@latest govulncheck ./... # gosec - статический анализ безопасности go install github.com/securego/gosec/v2/cmd/gosec@latest gosec ./... # Trivy - сканирование зависимостей и образов trivy fs --security-checks vuln ./ trivy image myapp:latest
9.2 Authentication vs Authorization
Разница
Authentication (аутентификация) — кто ты? Authorization (авторизация) — что тебе можно?
Пример:
- Authentication: логин по email+паролю, получение JWT токена
- Authorization: проверка что пользователь имеет право создать репо
Middleware для authentication
gofunc AuthMiddleware(tokenValidator TokenValidator) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { token := extractToken(r) if token == "" { writeProblem(w, http.StatusUnauthorized, "Unauthorized", "missing token") return } claims, err := tokenValidator.Validate(r.Context(), token) if err != nil { writeProblem(w, http.StatusUnauthorized, "Unauthorized", "invalid token") return } // Сохраняем пользователя в context ctx := context.WithValue(r.Context(), UserIDKey, claims.UserID) next.ServeHTTP(w, r.WithContext(ctx)) }) } }
Middleware для authorization
gofunc RequireRole(role string) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { userID := GetUserID(r.Context()) user, err := userService.GetUser(r.Context(), userID) if err != nil { writeProblem(w, http.StatusInternalServerError, "Error", "failed to get user") return } if user.Role != role { writeProblem(w, http.StatusForbidden, "Forbidden", "insufficient permissions") return } next.ServeHTTP(w, r) }) } }
Проверка владения ресурсом
Частая ошибка: пользователь может изменить чужой ресурс.
go// ПЛОХО: нет проверки владения func (h *Handler) DeleteRepo(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") h.repoService.Delete(r.Context(), id) // любой может удалить! } // ПРАВИЛЬНО: проверка владельца func (h *Handler) DeleteRepo(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") userID := GetUserID(r.Context()) repo, err := h.repoService.GetRepo(r.Context(), id) if err != nil { h.handleError(w, err) return } if repo.OwnerID != userID { writeProblem(w, http.StatusForbidden, "Forbidden", "not owner") return } h.repoService.Delete(r.Context(), id) }
RBAC (Role-Based Access Control)
gotype Role string const ( RoleAdmin Role = "admin" RoleUser Role = "user" RoleViewer Role = "viewer" ) type Permission struct { Resource string Action string } var rolePermissions = map[Role][]Permission{ RoleAdmin: { {Resource: "repos", Action: "create"}, {Resource: "repos", Action: "read"}, {Resource: "repos", Action: "update"}, {Resource: "repos", Action: "delete"}, }, RoleUser: { {Resource: "repos", Action: "create"}, {Resource: "repos", Action: "read"}, {Resource: "repos", Action: "update"}, }, RoleViewer: { {Resource: "repos", Action: "read"}, }, }
9.3 HMAC signing
Что такое HMAC
HMAC (Hash-based Message Authentication Code) — криптографическая конструкция для проверки целостности и подлинности сообщения.
Использует:
- Секретный ключ (известен обеим сторонам)
- Хеш-функцию (SHA-256, SHA-512)
- Сообщение
Когда использовать HMAC
- Webhooks (проверка что запрос от GitHub/GitLab)
- API между сервисами (service-to-service)
- Подпись payload в распределённых системах
- Проверка целостности сообщений
Реализация в Go
gopackage hmac import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "fmt" ) type Signer struct { secret []byte } func NewSigner(secret string) (*Signer, error) { if len(secret) < 32 { return nil, fmt.Errorf("secret must be at least 32 bytes") } return &Signer{secret: []byte(secret)}, nil } func (s *Signer) Sign(message []byte) string { mac := hmac.New(sha256.New, s.secret) mac.Write(message) return hex.EncodeToString(mac.Sum(nil)) }
Верификация подписи
gotype Verifier struct { secret []byte } func NewVerifier(secret string) (*Verifier, error) { if len(secret) < 32 { return nil, fmt.Errorf("secret must be at least 32 bytes") } return &Verifier{secret: []byte(secret)}, nil } func (v *Verifier) Verify(message []byte, signature string) error { mac := hmac.New(sha256.New, v.secret) mac.Write(message) expectedMAC := mac.Sum(nil) decoded, err := hex.DecodeString(signature) if err != nil { return fmt.Errorf("decode signature: %w", err) } // ConstantTimeCompare защищает от timing attacks if !hmac.Equal(decoded, expectedMAC) { return errors.New("invalid signature") } return nil }
Использование для webhook verification
gofunc (h *Handler) HandleWebhook(w http.ResponseWriter, r *http.Request) { // Читаем тело body, err := io.ReadAll(r.Body) if err != nil { writeProblem(w, http.StatusBadRequest, "Bad Request", "failed to read body") return } // Получаем подпись из заголовка signature := r.Header.Get("X-Signature-256") if signature == "" { writeProblem(w, http.StatusUnauthorized, "Unauthorized", "missing signature") return } // Проверяем подпись if err := h.verifier.Verify(body, signature); err != nil { writeProblem(w, http.StatusUnauthorized, "Unauthorized", "invalid signature") return } // Обрабатываем webhook // ... }
Отправка подписанного запроса
gofunc (c *Client) SendWebhook(ctx context.Context, url string, payload []byte) error { signature := c.signer.Sign(payload) req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(payload)) if err != nil { return fmt.Errorf("create request: %w", err) } req.Header.Set("Content-Type", "application/json") req.Header.Set("X-Signature-256", signature) req.Header.Set("X-Webhook-ID", generateID()) req.Header.Set("X-Webhook-Timestamp", strconv.FormatInt(time.Now().Unix(), 10)) resp, err := c.httpClient.Do(req) if err != nil { return fmt.Errorf("send webhook: %w", err) } defer resp.Body.Close() if resp.StatusCode >= 400 { return fmt.Errorf("webhook failed with status %d", resp.StatusCode) } return nil }
Защита от replay attacks
Злоумышленник может перехватить валидный запрос и повторить. Защита: timestamp + nonce + idempotency key.
go// При подписании добавляем timestamp message := fmt.Sprintf("%s.%s.%d", webhookID, body, timestamp) signature := signer.Sign([]byte(message)) // При верификации проверяем timestamp func (v *Verifier) VerifyWithTimestamp(body []byte, signature string, webhookID string, timestamp int64) error { // Проверяем что timestamp не старше 5 минут if time.Now().Unix() - timestamp > 300 { return errors.New("timestamp too old") } message := fmt.Sprintf("%s.%s.%d", webhookID, body, timestamp) return v.Verify([]byte(message), signature) }
9.4 Ротация секретов
Зачем нужна ротация
Секреты могут быть скомпрометированы:
- Утечка через логи
- Доступ бывшего сотрудника
- Взлом системы
- Компрометация секрет-менеджера
Ротация регулярно меняет секреты, снижая окно экспозиции.
Стратегия ротации HMAC ключей
Двухфазная ротация:
-
Фаза 1: добавить новый ключ, принимать подписи обоими
-
Фаза 2: прекратить использование старого ключа
type MultiKeyVerifier struct { currentKey []byte previousKey []byte // опционально, для периода ротации }
func (v *MultiKeyVerifier) Verify(message []byte, signature string) error { // Пробуем текущий ключ if err := verifyWithKey(v.currentKey, message, signature); err == nil { return nil }
// Пробуем предыдущий ключ (период ротации) if v.previousKey != nil { if err := verifyWithKey(v.previousKey, message, signature); err == nil { return nil } } return errors.New("invalid signature")}
HMAC rotator job
В шаблоне проекта есть hmac-rotator cron job:
gotype HMACRotatorJob struct { secretProvider repository.SecretProvider notifier Notifier logger *slog.Logger } func (j *HMACRotatorJob) Run(ctx context.Context) error { // Проверяем возраст текущего ключа metadata, err := j.secretProvider.GetMetadata(ctx, "hmac-key") if err != nil { return fmt.Errorf("get metadata: %w", err) } if time.Since(metadata.CreatedAt) < RotationPeriod { return nil // ещё рано } // Генерируем новый ключ newKey := generateSecureKey(64) // Сохраняем (с предыдущим ключом как previous) if err := j.secretProvider.Rotate(ctx, "hmac-key", newKey); err != nil { return fmt.Errorf("rotate: %w", err) } // Уведомляем команду j.notifier.Notify("HMAC key rotated", "New key active, previous valid for 24h") j.logger.Info("hmac key rotated") return nil }
Периодичность ротации
| Секрет | Периодичность |
|---|---|
| HMAC ключи для webhook | 30-90 дней |
| API ключи | 90 дней |
| TLS сертификаты | 90 дней (Let's Encrypt) |
| Пароли БД | 90 дней |
| OAuth токены | По истечении срока жизни |
9.5 Secrets management
Правила работы с секретами
- Никогда не коммитить секреты в git
- Использовать secret manager (Vault, Lockbox, AWS Secrets Manager)
- Передавать секреты через env в runtime
- Ротировать периодически
- Маскировать в логах
- Применять least privilege
Хранение секретов
go// ПЛОХО: секрет в коде const HMACSecret = "sk-1234567890abcdef" // ПЛОХО: секрет в git # config.prod.yaml hmac_secret: "sk-1234567890abcdef" // ПРАВИЛЬНО: секрет из env или secret manager hmacSecret := os.Getenv("HMAC_SECRET")
Использование secret manager (Lockbox)
gopackage lockbox import "context" type SecretProvider struct { client *lockbox.Client } func (p *SecretProvider) GetSecret(ctx context.Context, key string) (string, error) { secret, err := p.client.Get(ctx, key) if err != nil { return "", fmt.Errorf("get secret: %w", err) } return secret.Value, nil } func (p *SecretProvider) Rotate(ctx context.Context, key string, newValue string) error { return p.client.Update(ctx, key, newValue) }
Загрузка секретов при старте
gofunc loadSecrets(ctx context.Context, provider *lockbox.SecretProvider) (*Secrets, error) { hmacKey, err := provider.GetSecret(ctx, "hmac-key") if err != nil { return nil, fmt.Errorf("load hmac key: %w", err) } dbPassword, err := provider.GetSecret(ctx, "db-password") if err != nil { return nil, fmt.Errorf("load db password: %w", err) } return &Secrets{ HMACKey: hmacKey, DBPassword: dbPassword, }, nil }
9.6 Input validation
SQL injection
Атака: внедрение SQL-кода через пользовательский ввод.
go// УЯЗВИМО query := "SELECT * FROM users WHERE email = '" + email + "'" // email = "'; DROP TABLE users; --" // БЕЗОПАСНО: параметризованный запрос query := "SELECT * FROM users WHERE email = $1" row := pool.QueryRow(ctx, query, email)
Правило: всегда использовать параметризованные запросы.
XSS (Cross-Site Scripting)
Атака: внедрение JavaScript через пользовательский ввод.
go// УЯЗВИМО fmt.Fprintf(w, "Hello, %s", userInput) // userInput = "<script>alert('xss')</script>" // БЕЗОПАСНО: экранирование HTML import "html" fmt.Fprintf(w, "Hello, %s", html.EscapeString(userInput))
Для backend API XSS менее критичен, но нужно экранировать при генерации HTML.
CSRF (Cross-Site Request Forgery)
Атака: выполнение действия от имени авторизованного пользователя.
Защита:
- SameSite cookies (SameSite=Strict или Lax)
- CSRF tokens для state-changing операций
- Проверка Origin/Referer заголовков
- Требовать Content-Type: application/json (браузеры не могут отправить)
go// Установка SameSite cookie http.SetCookie(w, &http.Cookie{ Name: "session", Value: token, HttpOnly: true, Secure: true, SameSite: http.SameSiteStrictMode, })
Path traversal
Атака: доступ к файлам вне разрешённой директории.
go// УЯЗВИМО filePath := filepath.Join(baseDir, userInput) // userInput = "../../etc/passwd" // БЕЗОПАСНО: проверка что путь внутри baseDir filePath := filepath.Join(baseDir, filepath.Clean(userInput)) if !strings.HasPrefix(filePath, baseDir) { return errors.New("invalid path") }
Валидация URL для защиты от SSRF
gofunc ValidateWebhookURL(rawURL string) error { u, err := url.Parse(rawURL) if err != nil { return errors.New("invalid url") } // Только HTTPS if u.Scheme != "https" { return errors.New("only https allowed") } // Запрет частных IP host := u.Hostname() ip := net.ParseIP(host) if ip != nil && ip.IsPrivate() { return errors.New("private IP not allowed") } // Запрет localhost if host == "localhost" || host == "127.0.0.1" { return errors.New("localhost not allowed") } return nil }
9.7 Safe logging и PII
Что нельзя логировать
- Пароли и секреты
- Токены (JWT, API keys)
- Email, телефоны в открытом виде
- Платежные данные (PAN, CVV)
- Тело запроса с PII
- Заголовки Authorization, Cookie
Маскирование в логах
gofunc maskSecret(s string) string { if len(s) <= 8 { return "***" } return s[:4] + "***" + s[len(s)-4:] } // Использование logger.Info("api call", "api_key", maskSecret(apiKey), // abcd***xyzw )
Middleware для маскирования заголовков
gofunc SanitizeHeaders(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Удаляем чувствительные заголовки перед логированием r.Header.Del("Authorization") r.Header.Del("Cookie") next.ServeHTTP(w, r) }) }
PII masking через slog handler
gotype PIIMaskingHandler struct { next slog.Handler } func (h *PIIMaskingHandler) Handle(ctx context.Context, r slog.Record) error { // Проходим по всем атрибутам и маскируем PII r.Attrs(func(a slog.Attr) bool { if isPIIField(a.Key) { // Заменяем значение на маскированное // ... } return true }) return h.next.Handle(ctx, r) }
Audit logging
Для критичных операций ведём audit log:
gotype AuditEvent struct { Timestamp time.Time UserID string Action string Resource string Result string IPAddress string UserAgent string } func (s *RepoService) DeleteRepo(ctx context.Context, id string) error { userID := middleware.GetUserID(ctx) err := s.repo.Delete(ctx, id) // Audit log независимо от результата s.auditLogger.Log(AuditEvent{ Timestamp: time.Now(), UserID: userID, Action: "repo.delete", Resource: id, Result: errorToResult(err), IPAddress: middleware.GetIPAddress(ctx), }) return err }
9.8 Security headers и HTTPS
Обязательные security headers
gofunc SecurityHeaders(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("X-Content-Type-Options", "nosniff") w.Header().Set("X-Frame-Options", "DENY") w.Header().Set("X-XSS-Protection", "1; mode=block") w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin") w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains") w.Header().Set("Content-Security-Policy", "default-src 'self'") next.ServeHTTP(w, r) }) }
Значение заголовков
| Заголовок | Назначение |
|---|---|
| X-Content-Type-Options | Запрет MIME-sniffing |
| X-Frame-Options | Запрет встраивания в iframe |
| X-XSS-Protection | Защита от XSS в старых браузерах |
| Referrer-Policy | Контроль referrer |
| Strict-Transport-Security | Принудительный HTTPS (HSTS) |
| Content-Security-Policy | Разрешённые источники ресурсов |
HTTPS и TLS
В production всегда использовать HTTPS. Для Go сервиса обычно TLS терминируется на load balancer или reverse proxy (nginx, Envoy, ALB).
Для internal communication:
- mTLS (mutual TLS) между сервисами
- TLS для подключения к БД
- TLS для внешних API
go// Пример TLS клиента tlsConfig := &tls.Config{ MinVersion: tls.VersionTLS12, RootCAs: certPool, } transport := &http.Transport{ TLSClientConfig: tlsConfig, } client := &http.Client{ Transport: transport, Timeout: 30 * time.Second, }
9.9 Rate limiting
Зачем нужен rate limiting
- Защита от DoS-атак
- Защита от brute force
- Предотвращение злоупотреблений
- Fair use API
Алгоритмы rate limiting
| Алгоритм | Описание |
|---|---|
| Fixed window | Счётчик сбрасывается каждый интервал |
| Sliding window | Скользящее окно для плавности |
| Token bucket | Токены накапливаются, расходуются на запросы |
| Leaky bucket | Равномерная скорость обработки |
Token bucket реализация
gopackage ratelimit import ( "sync" "time" ) type TokenBucket struct { mu sync.Mutex tokens float64 maxTokens float64 refillRate float64 // токенов в секунду lastRefill time.Time } func NewTokenBucket(maxTokens int, refillRate float64) *TokenBucket { return &TokenBucket{ tokens: float64(maxTokens), maxTokens: float64(maxTokens), refillRate: refillRate, lastRefill: time.Now(), } } func (b *TokenBucket) Allow() bool { b.mu.Lock() defer b.mu.Unlock() now := time.Now() elapsed := now.Sub(b.lastRefill).Seconds() b.tokens += elapsed * b.refillRate if b.tokens > b.maxTokens { b.tokens = b.maxTokens } b.lastRefill = now if b.tokens >= 1 { b.tokens-- return true } return false }
Rate limit middleware
gofunc RateLimit(limiter *TokenBucket) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if !limiter.Allow() { w.Header().Set("Retry-After", "60") writeProblem(w, http.StatusTooManyRequests, "Too Many Requests", "rate limit exceeded") return } next.ServeHTTP(w, r) }) } }
Per-user rate limiting
gotype UserRateLimiter struct { mu sync.Mutex limiters map[string]*TokenBucket } func (l *UserRateLimiter) Allow(userID string) bool { l.mu.Lock() limiter, ok := l.limiters[userID] if !ok { limiter = NewTokenBucket(100, 10) l.limiters[userID] = limiter } l.mu.Unlock() return limiter.Allow() }
Для production использовать Redis-based rate limiter для распределённого лимитирования между инстансами.
Привязка к шаблону проекта
Где security в шаблоне
textinternal/security/ ├── hmac/ │ ├── verifier.go # HMAC verification │ ├── signer.go # HMAC signing │ └── rotation.go # Ротация ключей └── policy/ └── labels.go # Security labels internal/handlers/http/middleware/ └── pii.go # PII masking middleware
Что изучаем на примере шаблона
- HMAC signer и verifier
- Ротация ключей через cron job
- PII masking в логах
- Security policy и labels
Задание: изучите internal/security/
- Как реализован HMAC?
- Есть ли ротация ключей?
- Какие security policies?
- Как обрабатываются ошибки подписи?
💱 Компромиссы
- JWT vs серверные сессии: JWT проверяется локально через
TokenValidatorбез похода в БД, но отозвать токен до истечения срока жизни нельзя; сессия отзывается мгновенно, но каждый запрос оплачивается обращением к хранилищу. - Узкое окно timestamp vs задержанные webhooks: проверка «не старше 5 минут» надёжно режет replay attacks, но легитимный запрос, задержавшийся в пути, будет отклонён; широкое окно пропускает повторы и требует nonce с хранением.
- Rate limit по IP vs по
userID: лимит по IP защищает анонимные эндпоинты от brute force, но режет пользователей за общим NAT;UserRateLimiterчестнее для fair use, но требует аутентификации и Redis-лимитера между инстансами. - Двухфазная ротация vs одномоментная замена: приём подписей
обоими ключами (
currentKeyиpreviousKey) не ломает webhooks в полёте, но скомпрометированный старый ключ живёт ещё 24 часа; мгновенная замена сужает окно экспозиции, но рвёт интеграцию. - Подробные логи vs PII-маскирование:
maskSecretиPIIMaskingHandlerисключают утечки токенов и email в логи, но замаскированные значения уже не сопоставить при разборе инцидента; полные логи удобнее для аудита, но сами становятся источником утечки.
Практическое задание
Задание 1: HMAC Signer и Verifier
Реализуйте:
gotype Signer struct { secret []byte } func (s *Signer) Sign(message []byte) string func (s *Signer) Verify(message []byte, signature string) error
Протестируйте:
- Валидная подпись проходит
- Изменённое сообщение не проходит
- Неправильная подпись не проходит
- ConstantTimeCompare для timing safety
Задание 2: Webhook verification middleware
Реализуйте middleware для проверки HMAC подписи webhook:
gofunc WebhookAuth(verifier *hmac.Verifier) func(http.Handler) http.Handler
Middleware:
- Читает тело запроса
- Извлекает подпись из заголовка X-Signature-256
- Проверяет подпись
- Возвращает 401 при невалидной подписи
Задание 3: Auth middleware с JWT
Реализуйте JWT authentication middleware:
gofunc JWTAuth(validator TokenValidator) func(http.Handler) http.Handler
Middleware:
- Извлекает Bearer token из Authorization
- Валидирует JWT
- Сохраняет user ID в context
- Возвращает 401 при невалидном токене
Задание 4: Role-based authorization
Реализуйте RequireRole middleware:
gofunc RequireRole(role string) func(http.Handler) http.Handler
Middleware:
- Получает user ID из context
- Загружает пользователя
- Проверяет роль
- Возвращает 403 если нет прав
Задание 5: Security headers middleware
Реализуйте middleware с security headers:
gofunc SecurityHeaders(next http.Handler) http.Handler
Добавьте:
- X-Content-Type-Options: nosniff
- X-Frame-Options: DENY
- Strict-Transport-Security
- Content-Security-Policy
Задание 6: Rate limiting
Реализуйте rate limiter middleware:
gofunc RateLimit(requestsPerMinute int) func(http.Handler) http.Handler
Middleware:
- Ограничивает запросы per IP или per user
- Возвращает 429 при превышении
- Добавляет Retry-After заголовок
Задание 7: PII masking
Реализуйте middleware для маскирования PII:
gofunc PIIMasking(logger *slog.Logger) func(http.Handler) http.Handler
Middleware:
- Логирует запросы
- Маскирует email, телефоны в логах
- Не логирует Authorization и Cookie заголовки
Задание 8: Security check в CI
Добавьте в Makefile:
makefilesecurity: govulncheck ./... gosec ./...
Запустите и исправьте найденные проблемы.
Quiz "Проверь себя"
mac := hmac.New(sha256.New, secret)
mac.Write(payload)
expected := mac.Sum(nil)query := "SELECT * FROM repos WHERE name = '" + r.URL.Query().Get("name") + "'"
db.Exec(query)✅ Итоги модуля
Студент должен:
- Применять OWASP Top 10 на практике
- Различать authentication и authorization
- Реализовывать HMAC signing и verification
- Безопасно управлять секретами
- Защищаться от SQL injection, XSS, CSRF
- Правильно логировать без PII
- Настраивать security headers
- Реализовывать rate limiting
- Ответить на все вопросы Quiz
🔗 Мостик к Модулю 10
Следующий модуль: Модуль 10. Docker и контейнеризация - multi-stage Dockerfile, Docker Compose, best practices (non-root, slim images), health checks, security scanning (Trivy). Привязка к docker/ шаблона.