Опирается на правила: R-SEC-SECRET-1R-SEC-SECRET-3 и R-SEC-SECRET-X1R-SEC-SECRET-X2 из Security — индекс правил → раздел 3. Секреты в коде и истории.

Важно знать

  • Два слоя защиты: Gitleaks в pre-commit (до push) + CI (страховочно); ни один из них не заменяет другой.
  • Full history раз в неделю — паттерн мог не существовать при коммите; weekly-скан ловит старые утечки.
  • Pre-commit hook через .pre-commit-config.yaml — коммитится в репо, ставится одной командой; не ручная инструкция в README.
  • go env и os.Getenv не защищают — переменная с хардкодом в источнике равносильна хардкоду в коде.
  • Секреты в config.yaml / config.go — запрещены; только через envconfig из ENV/Vault/k8s Secret.
  • .env в git — запрещён, даже как «example с placeholder»; корректно — .env.example с пустыми значениями.
  • Rotation — первое действие при обнаружении утечки: GitHub уже проиндексировал, git filter-repo не возвращает скомпрометированный credential.
  • gosec G101 (HardCodedCredentials) и G304 (FilePathTraversal) ловятся автоматически при включённом gosec в golangci-lint.

Один закоммиченный API-ключ — это инцидент, даже если удалить через минуту. Malicious bots сканируют GitHub в режиме реального времени; AWS-ключи эксплуатируются в течение секунд после push. UCP формулирует двухслойную защиту: предотвращение через pre-commit + обнаружение в CI + быстрая rotation при утечке. В Go-стеке дополнительный риск — строковые литералы с секретами в config.go или функциях инициализации, которые gosec квалифицирует как G101.

Gitleaks в pre-commit

R-SEC-SECRET-1 — первый слой, ловит до push.

.gitleaks.toml в корне репо:

title = "orders-service gitleaks config"

[allowlist]
description = "Разрешённые паттерны"
paths = [
    '''\.example$''',
    '''^testdata/.*\.fixture\.json$''',
]

[[rules]]
id = "internal-api-key"
description = "Internal API key"
regex = '''internal_api_key\s*=\s*['"][a-zA-Z0-9]{32,}['"]'''
tags = ["key", "internal"]

[[rules]]
id = "jwt"
description = "JWT token"
regex = '''eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+'''

.pre-commit-config.yaml — hook коммитится в репо, ставится одной командой:

repos:
  - repo: https://github.com/gitleaks/gitleaks
    rev: v8.18.4
    hooks:
      - id: gitleaks

После pre-commit install — hook установлен у всех, кто клонирует репо. Без этого — README пишет «скопируйте файл в .git/hooks», большинство разработчиков пропускают шаг.

Альтернатива для проектов на Go без Node.js-окружения — pre-commit Python-framework или lefthook:

# lefthook.yml
pre-commit:
  commands:
    gitleaks:
      run: gitleaks detect --staged --config .gitleaks.toml

--staged сканирует только staged changes (то, что войдёт в коммит). Если finding — exit 1, коммит блокируется.

CI как страховочный слой

R-SEC-SECRET-1 — pre-commit можно обойти через git commit --no-verify; CI-шаг необходим как независимый контроль.

name: Gitleaks
on:
  pull_request:
  push:
    branches: [main]
  schedule:
    - cron: '0 3 * * 1'

jobs:
  gitleaks:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: gitleaks/gitleaks-action@v2
        env:
          GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }}

На PR — сканирует diff. Weekly cron (fetch-depth: 0) — сканирует full history: паттерн мог появиться в базе после того, как секрет был закоммичен.

envconfig вместо хардкода

R-SEC-SECRET-X1 — секреты в config.yaml или строковых литералах кода — только через переменные окружения.

В Go-стеке стандартный подход — структура конфига с тегами envconfig:

// config/config.go
package config

import "github.com/kelseyhightower/envconfig"

type Config struct {
    DatabaseURL    string `envconfig:"DATABASE_URL,required"`
    JWTSecret      string `envconfig:"JWT_SECRET,required"`
    SberAPIKey     string `envconfig:"SBER_API_KEY,required"`
    RedisURL       string `envconfig:"REDIS_URL,required"`
}

func Load() (Config, error) {
    var c Config
    return c, envconfig.Process("", &c)
}
// main.go
func main() {
    cfg, err := config.Load()
    if err != nil {
        slog.Error("load config", "err", err)
        os.Exit(1)
    }

    db, err := pgx.Connect(context.Background(), cfg.DatabaseURL)
    if err != nil {
        slog.Error("connect db", "err", err)
        os.Exit(1)
    }
    defer db.Close(context.Background())

    r := chi.NewRouter()
    // ...
}

Локально — .env.gitignore). В prod — Vault / k8s Secret / AWS Secrets Manager через runtime ENV.

os.Getenv допустим для опциональных конфигов, но для обязательных секретов лучше envconfig с required — при запуске без секрета сервис немедленно падает с явной ошибкой, а не в момент первого обращения к API.

Антипаттерн, который gosec G101 поймает как HardCodedCredentials:

// КАТАСТРОФА — gosec G101
const sberAPIKey = "sk-1234abcd-secret-key-prod"

func NewSberClient() *SberClient {
    return &SberClient{apiKey: sberAPIKey}
}

gosec G101 — автоматическое обнаружение

R-SEC-SAST-2gosec в golangci-lint ловит хардкод автоматически.

# .golangci.yml (фрагмент)
linters:
  enable:
    - gosec
linters-settings:
  gosec:
    severity: high
    confidence: high
gosec -fmt sarif -out gosec.sarif -severity HIGH ./...

gosec G101 срабатывает на литералы, присвоенные переменным с именами типа secret, password, key, token. SARIF-вывод загружается в GitHub Security tab через github/codeql-action/upload-sarif.

Если паттерн ложноположительный (например, тестовая фикстура, не настоящий ключ), подавление обязано содержать код нарушения, обоснование (≥ 30 символов) и срок:

//nolint:gosec // G101: тестовая фикстура, не production credential; убрать при переходе на testcontainers до: 2027-01-01
const testCustomerAPIKey = "test-key-fixture-not-real-credential"

.env.example без значений

R-SEC-SECRET-X2.env в git запрещён, даже с пометкой «example».

# ОШИБКА — .env.example с placeholder-значениями
JWT_SECRET=changeme
SBER_API_KEY=test-key-please-replace
DATABASE_URL=postgres://user:password@localhost/orders

Разработчик может не заменить значение и задеплоить changeme в production. Malicious bots scrape .env файлы из публичных репо.

Корректно — .env.example с пустыми значениями:

# .env.example (закоммитен)
DATABASE_URL=
JWT_SECRET=
SBER_API_KEY=
REDIS_URL=
# .env (в .gitignore, локальный)
DATABASE_URL=postgres://orders:dev@localhost:5432/orders_dev
JWT_SECRET=local-dev-only-secret
SBER_API_KEY=sandbox-key-from-sber-dev-portal
REDIS_URL=redis://localhost:6379

.gitignore:

.env
.env.local
.env.*.local
*.pem
*.key

Rotation — первое действие при утечке

R-SEC-SECRET-3 — порядок действий при обнаружении закоммиченного секрета.

Сценарий: Gitleaks нашёл SBER_API_KEY в коммите недельной давности.

Шаг 1 (в течение часа): rotate.

Deactivate скомпрометированный ключ в личном кабинете Sber API. Выпустить новый. Обновить ENV в кластере/Vault. Уведомить security-команду.

Шаг 2 (опционально, для compliance): удаление из истории.

git filter-repo --path-glob '*.env' --invert-paths
# или точечно
git filter-repo --replace-text <(echo "OLD_SECRET_VALUE==>REDACTED")

После filter-repo — force push и уведомить contributors о необходимости git clone заново.

Почему rotation первична: GitHub индексирует репо, боты сканируют новые коммиты непрерывно. Через минуту после push скомпрометированный credential может быть уже использован. git filter-repo помогает audit («в репо больше нет секрета»), но не возвращает уже утёкший ключ.

Что запрещено

АнтипаттернПравилоЧто взамен
Строковый литерал с секретом в config.goR-SEC-SECRET-X1envconfig с required тегом
Секрет в config.yaml / app.tomlR-SEC-SECRET-X1${ENV_VAR} или envconfig
.env в git (даже с changeme)R-SEC-SECRET-X2.env.example с пустыми значениями
Pre-commit hook — ручная инструкция в READMER-SEC-SECRET-2.pre-commit-config.yaml в репо
Gitleaks только в CI, без pre-commitR-SEC-SECRET-1оба слоя
Weekly full history scan отсутствуетR-SEC-SECRET-1cron в CI с fetch-depth: 0
git commit --no-verify как привычкаR-SEC-SECRET-2CI как независимый контроль
//nolint:gosec без кода и обоснованияR-SEC-SAST-X1код G101 + причина + срок
Rotation после удаления истории, не доR-SEC-SECRET-3rotate first, history cleanup — потом

Куда дальше

  • Образ и контейнер — секреты не в Docker ENV, base image с digest.
  • Криптография в коде — crypto/rand, AES-GCM, bcrypt cost ≥ 12.
  • CVE в зависимостях — govulncheck, Renovate, suppression со сроком.
  • Реакция на findings — SLA для leak: hotfix ≤ 1 час.
  • SAST по коду — gosec G101/G304, golangci-lint, SARIF в GitHub Security tab.