Опирается на правила:
R-SEC-SECRET-1…R-SEC-SECRET-3иR-SEC-SECRET-X1…R-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-2 — gosec в 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.go | R-SEC-SECRET-X1 | envconfig с required тегом |
Секрет в config.yaml / app.toml | R-SEC-SECRET-X1 | ${ENV_VAR} или envconfig |
.env в git (даже с changeme) | R-SEC-SECRET-X2 | .env.example с пустыми значениями |
| Pre-commit hook — ручная инструкция в README | R-SEC-SECRET-2 | .pre-commit-config.yaml в репо |
| Gitleaks только в CI, без pre-commit | R-SEC-SECRET-1 | оба слоя |
| Weekly full history scan отсутствует | R-SEC-SECRET-1 | cron в CI с fetch-depth: 0 |
git commit --no-verify как привычка | R-SEC-SECRET-2 | CI как независимый контроль |
//nolint:gosec без кода и обоснования | R-SEC-SAST-X1 | код G101 + причина + срок |
| Rotation после удаления истории, не до | R-SEC-SECRET-3 | rotate 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.