Що ви подумаєте, якщо я вам скажу, що зберігати паролі, токени та іншу чутливу інформацію в репозиторії — нормально?
Звичайно, є одне “але”: нормально — коли значення зашифровані, а не лежать у відкритому вигляді. І це доволі легко зробити за допомогою SOPS.
Проблема, яку вирішує SOPS
Класична дилема з секретами виглядає так. Або секрети лежать у репозиторії в чистому вигляді — і тоді будь-хто з доступом до репо (а після першого ж витоку — будь-хто взагалі) читає ваші паролі від продакшн-бази. Або секрети живуть окремо від коду — у Vault, AWS Secrets Manager чи ще якомусь сховищі — і тоді починаються незручності:
- конфіг і секрет до нього змінюються в різних місцях: PR окремо, секрет окремо;
- історія змін розпадається на два паралельні треки, і “хто і коли поміняв цей токен” вже не видно в git log;
- рев’ю конфігурації не бачить секретної частини змін;
- для локальної розробки і CI потрібні окремі механізми доставки секретів.
А якщо ви живете за GitOps-підходом, то він взагалі вимагає, щоб усе було декларативним і лежало в git — включно з секретами. Але точно не у відкритому вигляді.
SOPS дозволяє всидіти на обох стільцях: секрети лежать у git поруч з конфігами, проходять той самий PR-процес — але в зашифрованому вигляді.
Що це таке
SOPS розшифровується як Secrets OPerationS. Інструмент з’явився у Mozilla ще у 2015 році, у 2023-му був переданий до CNCF як Sandbox-проєкт і зараз розвивається спільнотою під парасолькою getsops.
Технічно це утиліта-редактор для шифрування файлів: YAML, JSON, ENV, INI та бінарних. Але головна фішка, яка відрізняє SOPS від підходу “зашифрував файл цілком і закомітив блоб”:
SOPS шифрує значення, а не ключі. Структура файлу лишається читабельною — видно, які поля там взагалі є; diff у PR показує, яке саме поле змінилось; мерж-конфлікти лишаються керованими. Ось як виглядає зашифрований YAML:
database:
host: db.internal
username: app
password: ENC[AES256_GCM,data:Ekb+Fbmz...,tag:...]
host і username — читабельні, бо вони не секрет. Зашифрований лише password — і на рев’ю видно, що змінився саме він.
Для самого шифрування SOPS підтримує кілька бекендів:
- офлайн-ключі: age — сучасний і рекомендований за замовчуванням варіант, та PGP/GnuPG — легасі з відомими болями ротації ключів;
- хмарні KMS: AWS KMS, GCP KMS, Azure Key Vault, HuaweiCloud KMS — зручно, коли доступ до секретів має контролюватись IAM-політиками хмари;
- HashiCorp Vault / OpenBAO — transit-шифрування, якщо Vault у вас вже є.
Бекенди можна комбінувати: один файл може бути зашифрований одночасно, наприклад, age-ключем розробника і AWS KMS для CI — розшифрує будь-хто з них.
Як це виглядає на практиці
Спочатку — як це виглядає вживу: файл із секретами, правила в .sops.yaml, шифрування на місці та точкове розшифрування одного значення.

Тепер по кроках. Генеруємо ключову пару age:
age-keygen -o key.txt
export SOPS_AGE_KEY_FILE=$PWD/key.txt
У корені репозиторію кладемо .sops.yaml — правила, що і яким ключем шифрувати:
creation_rules:
- path_regex: .*\.yaml$
encrypted_regex: ^(password|token)$
age: age1... # публічний ключ з key.txt
encrypted_regex каже шифрувати лише поля password і token — решта лишається відкритим текстом. Маємо файл із секретами:
app:
name: demo-app
environment: production
database:
host: db.internal
username: app
password: SuperSecret123!
api:
url: https://api.example.com
token: glpat-verySecretToken
Шифруємо на місці:
sops encrypt --in-place secrets.yaml
Після цього файл можна спокійно комітити: структура і несекретні поля читабельні, секретні значення — ENC[AES256_GCM,...], плюс у файлі з’являється службова секція sops з метаданими (зашифрований data-ключ, MAC для контролю цілісності).
Далі з файлом працюють так:
sops secrets.yaml # відкриє розшифрований файл у $EDITOR,
# при збереженні зашифрує назад
sops decrypt secrets.yaml # розшифрувати в stdout
sops decrypt --extract '["database"]["password"]' secrets.yaml # одне значення
Тобто для людини з ключем робота з секретами майже не відрізняється від роботи зі звичайним файлом — шифрування прозоре.
Усі команди з цього демо лежать у моєму репозиторії скриптів до статей: gitlab.com/punkoivan/punka-space-scripts — можна відтворити локально, там же і VHS-сценарій, яким записана гіфка.
CI/CD та GitOps
Локальний age-ключ — це добре для демо і невеликих команд, але в пайплайнах зазвичай зручніше хмарний KMS: CI-джоба отримує роль/сервіс-акаунт з правом Decrypt на KMS-ключ і розшифровує секрети без жодного приватного ключа в змінних CI.
У Kubernetes-світі SOPS теж давно свій:
- Flux підтримує SOPS з коробки — kustomize-контролер сам розшифровує маніфести перед застосуванням, є офіційний гайд;
- ArgoCD нативної підтримки не має, але є усталені інтеграції через плагіни (ksops, helm-secrets).
В обох випадках схема однакова: секрети живуть у git як SOPS-зашифровані маніфести, а право на розшифрування має лише контролер у кластері.
А чому не Vault чи Sealed Secrets?
Коротке порівняння з альтернативами
Sealed Secrets (Bitnami) — Kubernetes-специфічне рішення: шифрує весь об’єкт Secret публічним ключем контролера в кластері. Працює добре, але тільки для K8s і тільки для Secret-об’єктів. SOPS універсальніший: будь-який YAML/JSON/ENV/INI, хоч values.yaml для Helm, хоч .env для docker-compose, і diff показує змінене поле, а не один суцільний блоб.
HashiCorp Vault — це взагалі інша вагова категорія: секрети не в git, динамічна видача креденшлів, ротація на льоту, детальний аудит-лог доступу. Але це й окремий сервіс, який треба розгортати, обслуговувати і берегти як зіницю ока.
SOPS — не заміна Vault. Це рішення для випадків, коли секрет має жити поруч з конфігом у git і проходити той самий review/PR-процес. Якщо вам потрібні динамічні креденшли і аудит кожного читання секрету — це до Vault. Якщо потрібно, щоб values.yaml з парою токенів безпечно лежав у репозиторії — SOPS значно дешевший у володінні.
Ризики та підводні камені
Куди ж без цього. SOPS знімає одні ризики, але додає нові точки, які варто врахувати в моделі загроз:
- Втрата приватного ключа = втрата всіх зашифрованих секретів. Кнопки “забув пароль” тут немає. Резервні копії ключів мають існувати — і зберігатись окремо від репозиторію. Хороша практика — шифрувати кожен файл кількома отримувачами (наприклад, ключі кількох людей + KMS), щоб один втрачений ключ не став катастрофою.
.sops.yamlлегко налаштувати неправильно. Якщоencrypted_regexне покриває якесь чутливе поле — воно поїде в git відкритим текстом, і виглядатиме це цілком легально: файл же “зашифрований”. Правила варто рев’ювити так само прискіпливо, як і самі секрети, а на CI можна додати перевірку, що у файлах немає незашифрованих підозрілих полів.- Право на розшифрування — це і є секрет. Доступ до KMS-ключа чи age-приватного ключа еквівалентний доступу до всіх секретів, які ним зашифровані. Тобто контроль доступу нікуди не зникає — він переїжджає на рівень керування ключами: IAM-політики на KMS, зберігання age-ключів, offboarding людей з їхніми ключами (до речі, після відкликання ключа секрети треба не лише перешифрувати, а й ротувати — старі значення людина вже бачила).
- Секрет розшифровується там, де використовується. SOPS захищає секрети в репозиторії — але в рантаймі (CI-джоба, под у кластері) вони все одно з’являються у відкритому вигляді. Логи, дампи і артефакти пайплайнів лишаються класичними каналами витоку.
Підсумок
SOPS — простий спосіб зробити так, щоб секрети жили в git разом з конфігурацією: зашифровані значення, читабельна структура, робочі diff’и та звичний PR-процес. Поріг входу мінімальний — age-ключ і .sops.yaml на п’ять рядків, а далі можна дорощувати до KMS і GitOps-інтеграцій.
Головне пам’ятати: ви не позбулись задачі керування доступом — ви перенесли її з секретів на ключі. Але ключів сильно менше, ніж секретів, і керувати ними значно легше.
Скрипти з усіма командами зі статті — у punka-space-scripts.