Secret Management
В данном практическом занятии познакомимся с инструментами для локального управления чувствительными данными(пароли, ключи, токены и т.д.).
Vagrant
Vagrant.configure("2") do |config|
config.vm.define "node" do |c|
c.vm.box = "ubuntu/lunar64"
c.vm.hostname = "node"
c.vm.network "private_network", type: "dhcp"
c.vm.provision "shell", inline: <<-SHELL
apt-get update -q
apt-get install -yq libnss-mdns ansible
curl -L https://github.com/FiloSottile/age/releases/download/v1.1.1/age-v1.1.1-linux-amd64.tar.gz \
| tar xvz --strip-components=1 -C /usr/local/bin age/age age/age-keygen
curl -L https://github.com/getsops/sops/releases/download/v3.8.1/sops-v3.8.1.linux.amd64 \
-o /usr/local/bin/sops && chmod +x /usr/local/bin/sops
SHELL
end
end
Ansible Vault
Различные стеки технологий часто имеют собственные инструменты для управления
секретами, в ansible таким инструментом является ansible-vault. Утилита
ansible-vault
позволяет шифровать данные парольной фразой, так что при утечке
не будет возможности воспользоваться секретными данными.
Create
Для создания секрета можно воспользоваться подкомандой create
, которая запросит
пароль для нового файла и откроет текстовый редактор, определенный в переменной
EDITOR
:
$ EDITOR=nano ansible-vault create secret.yaml
New Vault password:
Confirm New Vault password:
/home/vagrant/.ansible/tmp/ansible-local-71380mr2u0wp/tmpfx3ulqj0.yaml *
item: secret_data
list:
- secret_item1
- secret_item2
- secret_item3
dict:
key1: secret_value
key2: secret_value
^G Help ^O Write Out ^W Where Is ^K Cut ^T Execute ^C Location
^X Exit ^R Read File ^\ Replace ^U Paste ^J Justify ^/ Go To Line
Внесем данные, которые хотим зашифровать и сохраним файл.
Если на текущий момент посмотреть данный файл, то содержимое будет в зашифрованном виде:
$ cat secret.yaml
$ANSIBLE_VAULT;1.1;AES256
31613863313562623337353430663066646236643639626635356135653165656130343036656130
6638653365303364623965366461626262373064353462610a356164393364363266343264633938
65323664353434616438336163636165393434633333643433636435636262353338663839626330
3138663266346232650a373661333662366166363739303032373430373731316237386232653133
65303366363064373039643730653339396439353736376635626434653536633361666665643938
63373231383166363136303566316438323438333837323366623161346332376562346130336630
35316461623263663236313961383636663936336334616132663066343562616534646163373837
35616265313031346638653834666533393164636334643438363634303565656138373030396666
33363035323065386166353663653462653335383232376634643064343564343338383837326666
3932306430363162613064346437303630633239656136353831
View
Для вывода на экран расшифрованное содержимое можно воспользоваться подкомандой
view
:
$ ansible-vault view secret.yaml
Vault password:
item: secret_data
list:
- secret_item1
- secret_item2
- secret_item3
dict:
key1: secret_value
key2: secret_value
Edit
Отредактировать же можно подкомандой edit
, которая также откроет текстовый
редактор как в команде create
:
$ ansible-vault edit secret.yaml
Vault password:
item: secret_data
list:
- secret_item1
- secret_item2
- secret_item3
dict:
key1: secret_value1
key2: secret_value2
~
~
<ocal-71616c9rq5j8/tmpjkj4ar9i.yaml" 8L, 119B written 8,21 All
Encrypt/Decrypt
Расшифровать и зашифровать файл на месте можно соответственно подкомандами
decrypt
и encrypt
:
$ cat secret.yaml
$ANSIBLE_VAULT;1.1;AES256
34366264616236613863303231396132373635323130323065396661633236363866663634393236
3363316662656535633930636632613733373932353935350a363766353231373364346230646634
31363936663633373630326131356435323037663533616265326234643264353631316166373761
3932386630653432620a623063383932623436326663633435393232316166393130613465393231
39346339333839653232323431346539616362326139356636356639316630666435376233616431
32666438356466346633633835363232383534316164343636633136313863643163666663383766
30313130376161386366636565653334313036313332313435356330636234633732616366653263
63393035383765623562626531633238306133633066666434386666656131666663623062356366
37623233623162313339333237356533326566663038323561336138393462386633616538323436
3633663062373631666539653036633034383834376161366161
$ ansible-vault decrypt secret.yaml
Vault password:
Decryption successful
$ cat secret.yaml
item: secret_data
list:
- secret_item1
- secret_item2
- secret_item3
dict:
key1: secret_value1
key2: secret_value2
$ ansible-vault encrypt secret.yaml
New Vault password:
Confirm New Vault password:
Encryption successful
$ cat secret.yaml
$ANSIBLE_VAULT;1.1;AES256
64323737633536313039393432653135396563333538623538376133383062323961353033346330
3439633434393039303633393066373437643938626130320a326466323566306535373231636639
65653833613634313835646339303935653336663037306238386334373661393830633736383866
3039363536303833640a373735636334373535383034666533633533316539646165333034343739
66383335613736316565356535383137373562386664656232636332616261623132353834356433
31626332623033613535383264646534393436356230386262323135353738343634393939656531
38663339383330656637623037663930386539383639336337646230373534623165656665376562
64366463343939613863643333313538623365366632393131323137623535666164343565376361
30343538356464323461643734363432643935653263336631343062333835396161313135343164
3734643933313330396133643230646639383439386566396436
Playbook
Зашифрованные файлы с помощью ansible-vault
можно использовать в плейбуках.
Создадим файл playbook.yaml
, который будет использовать наш зашифрованный файл:
---
- hosts: localhost
connection: local
gather_facts: False
vars_files:
- secret.yaml
tasks:
- name: test
debug:
msg: "{{ list }}"
И запустим его указав опцию --ask-vault-pass
:
$ ansible-playbook playbook.yaml --ask-vault-pass
Vault password:
PLAY [localhost] *****************************************************************
TASK [test] **********************************************************************
ok: [localhost] => {
"msg": [
"secret_item1",
"secret_item2",
"secret_item3"
]
}
PLAY RECAP ***********************************************************************
localhost : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Таким образом во время запуска мы можем получить доступ к секретным данным и использовать их в плейбуках.
Encrypt string
Также с помощью подкоманды encrypt_string
можно зашифровать переданную строку,
которую можно указать как аргумент или же через стандартный ввод:
$ ansible-vault encrypt_string
New Vault password:
Confirm New Vault password:
Reading plaintext input from stdin. (ctrl-d to end input, twice if your content does not already have a newline)
secretString
Encryption successful
!vault |
$ANSIBLE_VAULT;1.1;AES256
65623232363334306166353539343338613238646362643462356237626130383064363932363135
6166396634326366323666613339646262323935346534330a626137666161376538373661393733
62646535663135303733626336393837363233396162383864333263393939393039663465386232
3334653638656537330a386365653664336333633134303235393134313161373936333161366636
После чего вывод команды также можно использовать в плейбуке:
---
- hosts: localhost
connection: local
gather_facts: False
vars:
data: !vault |
$ANSIBLE_VAULT;1.1;AES256
61616430323334633331373333363932376264626439346465623139336363616161656132346164
3330316666323662303665343261323535303738303861660a623538313730653137333364353836
66353866646534623536326435373362386134653862383430313930343933663961666239343064
3635633834383536320a323866396462653830623338656334353430303639353836376232643563
6266
tasks:
- name: test
debug:
msg: "{{ data }}"
$ ansible-playbook playbook.yaml --ask-vault-pass
Vault password:
PLAY [localhost] *****************************************************************
TASK [test] **********************************************************************
ok: [localhost] => {
"msg": "secretData\n"
}
PLAY RECAP ***********************************************************************
localhost : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Age
Также познакомимся с утилитой age, которая не привязана ни к какому стеку и позволяющая очень просто использовать ключи шифрования помимо парольной фразы для шифрования локальных файлов.
В качестве файла возьмем secret.yaml
из предыдущего, не забыв расшифровать его,
если он был зашифрован утилитой ansible-vault
:
$ ansible-vault decrypt secret.yaml
Vault password:
Decryption successful
$ cat secret.yaml
item: secret_data
list:
- secret_item1
- secret_item2
- secret_item3
dict:
key1: secret_value1
key2: secret_value2
Keygen
Для генерации ключа воспользуемся командой age-keygen
и сохраним его в файл
key.txt
:
$ age-keygen -o key.txt
Public key: age1m7dhpf204lsx6d5h56s0e96nudvjqaa2g929ec0y83tncu6umpxq7egxmp
$ age-keygen -y key.txt
age1m7dhpf204lsx6d5h56s0e96nudvjqaa2g929ec0y83tncu6umpxq7egxmp
Как видно при генерации ключа на экран выводится его публичная часть, которую
также можно посмотреть с помощью опции -y
у уже созданного ключа.
Encrypt
Для шифрования файла как раз необходима публичная часть ключа, которая указывается
в опции -r
команды age
:
$ # сохранить зашифрованный файл в бинарном формате
$ age -r age1m7dhpf204lsx6d5h56s0e96nudvjqaa2g929ec0y83tncu6umpxq7egxmp -o secret.yaml.age secret.yaml
$ # сохранить зашифрованный файл в PEM формате
$ age -r age1m7dhpf204lsx6d5h56s0e96nudvjqaa2g929ec0y83tncu6umpxq7egxmp -a secret.yaml > secret.yaml.age
$ cat secret.yaml.age
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBrVEVpc2dmYUgvYU1FaERO
UEtzcCtQbDlyVW9CZkVFMC9SVXlqbk5iWEhrCjVPbUdYQ2pMTFhYaWhtWkZsVmh6
Y3ZUdE0rV21yMkpIS3k0bUJSdGMzdjQKLS0tIFZrR3lwZVRud2F0ZXNuVHFHaHpz
bTE2QStkS2wvNzJwcWVRdHFaVjF5WEEKZEyOM+f8EcO8ZD3j5YLJeHffJiRW+ZCD
FBy1PpyTkZ8DyZJvY1oDeIUJRD759eOrrR933xIj6ykwUpD1UiO5cUF8KRqHRdgr
aSjIKrixWs3Y84saca+p+z67nLMSs3vpnB8TdrvKS65GWsHGQ3gvEzDnDqhMV4DC
pIRRr5xsEmtjhezQoC0h/Q2apcLKSwt5ILoZ14RMNQ==
-----END AGE ENCRYPTED FILE-----
Decrypt
Для расшифровывания файла необходимо использовать ключ -d
и -i
с указанием
файла ключа:
$ age -d -i key.txt secret.yaml.age
item: secret_data
list:
- secret_item1
- secret_item2
- secret_item3
dict:
key1: secret_value1
key2: secret_value2
SSH Key
Для шифрования также есть возможность использовать ssh ключи, которые обычно есть у любого разработчика:
$ ssh-keygen -P '' -f $HOME/.ssh/id_rsa
Generating public/private rsa key pair.
Your identification has been saved in /home/vagrant/.ssh/id_rsa
Your public key has been saved in /home/vagrant/.ssh/id_rsa.pub
The key fingerprint is:
SHA256:CWCVKKgOUcVJ1WUp/ct8we8pVDsLQdI3dL8M8upx72k vagrant@node
The key's randomart image is:
+---[RSA 3072]----+
| o.+==oo oo.. ...|
|o ..+.. o.o. o oo|
|.. . . . ooo. o|
|o . . +.=..|
|o S o oo=.|
| . =o.o.|
| o.o..+|
| . o..Eo|
| . ++ |
+----[SHA256]-----+
$ age -R $HOME/.ssh/id_rsa.pub -a secret.yaml > secret.yaml.age
$ age -d -i $HOME/.ssh/id_rsa secret.yaml.age
item: secret_data
list:
- secret_item1
- secret_item2
- secret_item3
dict:
key1: secret_value1
key2: secret_value2
Multiple keys
А также для шифрования можно одновременно использовать несколько ключей, комбинируя как age ключи так и ssh:
$ age -r age1m7dhpf204lsx6d5h56s0e96nudvjqaa2g929ec0y83tncu6umpxq7egxmp \
-R $HOME/.ssh/id_rsa.pub -a secret.yaml > secret.yaml.age
$ age -d -i $HOME/.ssh/id_rsa secret.yaml.age
item: secret_data
list:
- secret_item1
- secret_item2
- secret_item3
dict:
key1: secret_value1
key2: secret_value2
$ age -d -i key.txt secret.yaml.age
item: secret_data
list:
- secret_item1
- secret_item2
- secret_item3
dict:
key1: secret_value1
key2: secret_value2
Таким образом можно хранить файл с открытыми ключами нескольких пользователей, например, там же где и файл с секретами. При этом каждый пользователь сможет читать, изменять и перешифровывать такой файл без знания общей парольной фразы.
Sops
Еще один инструмент для управления секретами - SOPS: Secrets OPerationS. Данный инструмент использует внешнего поставщика ключей для шифрования - это могут быть age и gpg как локальные утилиты, так и облачные KMS сервисы.
GPG
Рассмотрим работу sops в связке с pgp ключами. Для этого с помощью утилиты gpg создадим себе ключевую пару.
$ gpg --quick-gen-key --pinentry-mode=loopback --passphrase='' Alex default default 0
gpg: directory '/home/vagrant/.gnupg' created
gpg: keybox '/home/vagrant/.gnupg/pubring.kbx' created
gpg: /home/vagrant/.gnupg/trustdb.gpg: trustdb created
gpg: directory '/home/vagrant/.gnupg/openpgp-revocs.d' created
gpg: revocation certificate stored as '/home/vagrant/.gnupg/openpgp-revocs.d/4D9DC3B8A580D927DEC9D4325A66620A06DF7D54.rev'
public and secret key created and signed.
pub rsa3072 2023-11-15 [SC]
4D9DC3B8A580D927DEC9D4325A66620A06DF7D54
uid Alex
sub rsa3072 2023-11-15 [E]
Encrypt
Для шифрования с помощью pgp необходимо использовать отпечаток ключа. Можно
его передавать через аргументы или переменную среды, но удобнее использовать
файл конфигурации, который также можно хранить вместе с проектом, где будут
храниться секреты. Создадим файл .sops.yaml
в который добавим отпечаток ключа:
creation_rules:
- pgp: >-
4D9DC3B8A580D927DEC9D4325A66620A06DF7D54
Sops умеет работать со структурированными форматами файлов такие как
json
, yaml
, env
, таким образом позволяя частично шифровать данные.
Тип файла по-умолчанию определяется по расширению, так что, для того чтобы
зашифровать yaml
файл полностью, необходимо указать его тип как binary
:
$ sops -e --input-type binary secret.yaml
data: ENC[AES256_GCM,data:zpeGBft25RTki9dM9wzezpWBUstltnpsG2AM89kl9ZcL3ovj+8ugiQNMc0NaLDZo6OZjbCD6WcDrUQZTMgr8l8DIVhlQE1wcachu3tc4QLo2Ha77Sf37tXry8EgBknShtK3V1kcTgJZ/R0tahL4n1zdVr165FIk=,iv:9uvmPz5fuYc9OY+gWEmAWqUJn2Rgw1Rlf+05VbTr7Oc=,tag:ueirLeHOMfHHC1hBWOZtOg==,type:str]
sops:
kms: []
gcp_kms: []
azure_kv: []
hc_vault: []
age: []
lastmodified: "2023-11-15T20:20:22Z"
mac: ENC[AES256_GCM,data:A+eNt5DtQiISq6YEYyUwn2tG4Loxsw2AWeLgfpsffMR0u+6Ob7NECn/cRjF6Ik5Drun2CYCla0/vRbpPi1aVvyfVpxTMaLWyJJdErX6nxZ00ojqlwM1ehPJaRPSUStuTuV9uiqAMCwt3DVHwiRQUNKP0S7W1N41HC1XijfI3iNw=,iv:0YEdmAiDVDYiprZV5yyddpwqRJzLUS5aDDHsQYkThKM=,tag:OLi6Y3VKScG6lC36dAY/3w==,type:str]
pgp:
- created_at: "2023-11-15T20:20:22Z"
enc: |-
-----BEGIN PGP MESSAGE-----
hQGMA87ESxgiPDlNAQv8DxE8QWoASICvC1P08kejqe8zdBeY0HkSbhpbPbu1CnxO
vPkp16XFa1EyR2nSR66tRubq6PYuQeZ1yJuPL55zA80Iu7WhUNDgXNc9/aBaEMTz
/kQ/LXfA5eN6vXP1NO7WTuh7x7BiQi+N3+HOYVYWYleckzgffpHiLIGSM5LpLPBt
W2OK3NVw/s/hQsXv04AqpKTZCm2o97LvpytceiGylhKWd8MacM2wnHBLs+Sz9Ubq
dMawcvipCx03FL5BFSDVbaZ4UK7BDMr/fAXF5l5EO4fIj8ghICjJN7wgWutTKUZq
BK/dGFp/FiDZ4SLCRr+N9Eo5v45aFA3eLP71QHSfAitKKnqlQ7pER0Y9BdBVGBaj
9v3HU3x7ebObRDaHdhuCACWOyJPiQaEHVAnH2dTrkwGEkIku89DROIFQOjEEOi96
rGdGDRX+bY0qHUSomZ/wiQNCL2DBg/PWwxqBb5F84DJiQ9pH7McTiO0CHro9prz6
SpCdEJ+5KPWIWTDIG55f0lwBmriqg8BjYn3O5HYHXCBgveWi9LpROcUTUJPqsjBW
SdrWTMsVywXZUAjw9JldMLIFKyZFKTmdnV2LhxSAi8icXAWOboz+qV9RaunVPMI0
VJ/RPTXrq/B3G+MvWQ==
=mGt8
-----END PGP MESSAGE-----
fp: 4D9DC3B8A580D927DEC9D4325A66620A06DF7D54
unencrypted_suffix: _unencrypted
version: 3.8.1
Как видно зашифрованное содержимое выводится на экран по-умолчанию, для сохранения
в другой файл можно использовать опцию --output
, либо же опцию -i
(--in-place
)
для изменения текущего файла. Попробуем использовать частичное шифрование
структурированного файла изменив его содержимое:
$ sops -e -i secret.yaml
$ cat secret.yaml
item: ENC[AES256_GCM,data:6C16dZrteSU++Kk=,iv:SPmmwTlhDP6wkY1w4S6rosNQoE7XlG59D/WXoa0sPZg=,tag:SGwzUMqIVWOOro4oQyypyg==,type:str]
list:
- ENC[AES256_GCM,data:PiLZhs/ED3q3dMfc,iv:XsfueJtWRfKj7Opx4pZd2ViUQtrbLIXizS8ETGtUafw=,tag:RdBMCOppAZoNLgt4daBU7w==,type:str]
- ENC[AES256_GCM,data:D25wWO4I9drqxCTS,iv:C+mxtvYas+N7+BhCaqji4p3zodQLBYCZ617YDz4j100=,tag:+wGmRERrjnzr9hnkLTnHNw==,type:str]
- ENC[AES256_GCM,data:SqMsPlWAtVYCsoE4,iv:Z06k7lccdL8NrdDCXjjpqfWp+/zqkzwAVfUZeFOo+E0=,tag:7e/60SQ41oYt/8xbPVE4YA==,type:str]
dict:
key1: ENC[AES256_GCM,data:ghZWn233f1DcWTpLNw==,iv:1TnxUTA4KPVVhVmE22wqRGb2QG5h3FEwmBLZlh821z4=,tag:MkrdB1u5W5EHGPgRLjucmQ==,type:str]
key2: ENC[AES256_GCM,data:LP7JVUQnk6NGKIeEVQ==,iv:X1YSuh/Fum2E5E7mQco9bi335WreI6DdqM17Zvksk70=,tag:PqjofaBgFZ1rEtuHAI1loA==,type:str]
sops:
...
Edit
Если использовать команду sops
передав лишь имя зашифрованного файла, то
по-умолчанию открывается текстовый редактор определенный в переменной EDITOR
с расшифрованным содержимым файла, который можно отредактировать и он повторно
будет зашифрован:
$ EDITOR=nano sops secret.yaml
GNU nano 7.2 /tmp/1481366927/secret.yaml *
item: secret_data
list:
- secret_item1
- secret_item2
- secret_item3
dict:
key1: secret_value3
key2: secret_value2
[ Read 8 lines ]
^G Help ^O Write Out ^W Where Is ^K Cut ^T Execute ^C Location
^X Exit ^R Read File ^\ Replace ^U Paste ^J Justify ^/ Go To Line
Set/Extract
С помощью опции --extract
вместе с опцией -d
можно расшифровать только
нужные части файла указав путь до них:
$ sops -d --extract '["item"]' secret.yaml
secret_data
$ sops -d --extract '["list"][0]' secret.yaml
secret_item1
А с помощью опции --set
можно таким же образом устанавливать значения:
$ sops -d --extract '["dict"]["key1"]' secret.yaml
secret_value3
$ sops --set '["dict"]["key1"] "secret_value1"' secret.yaml
$ sops -d --extract '["dict"]["key1"]' secret.yaml
secret_value1
Таким образом возможно удобное использование в скриптах и CI/CD пайплайнах.
Exec File/Env
С помощью подкоманды exec-file
можно вызвать какое-либо другое приложение,
которому передастся расшифрованный временный файл:
$ sops exec-file secret.yaml 'cat {}'
item: secret_data
list:
- secret_item1
- secret_item2
- secret_item3
dict:
key1: secret_value1
key2: secret_value2
Также есть возможность запустить приложение задав ему переменные среды из зашифрованного файла:
$ echo -e "VAR1=VALUE1\nVAR2=VALUE2" > .env
$ sops -e -i .env
$ sops exec-env .env 'sh -c "echo $VAR1 $VAR2"'
VALUE1 VALUE2
Update keys
Для изменения ключей шифрования, например когда нужно добавить ключи новых
пользователей или удалить старых, есть подкоманда updatekeys
. Добавим наш
age ключ в .sops.yaml
:
creation_rules:
- pgp: >-
4D9DC3B8A580D927DEC9D4325A66620A06DF7D54
age: age1m7dhpf204lsx6d5h56s0e96nudvjqaa2g929ec0y83tncu6umpxq7egxmp
И обновим:
$ sops updatekeys secret.yaml
2023/11/15 21:32:44 Syncing keys for file /vagrant/secret.yaml
The following changes will be made to the file's groups:
Group 1
4D9DC3B8A580D927DEC9D4325A66620A06DF7D54
+++ age1m7dhpf204lsx6d5h56s0e96nudvjqaa2g929ec0y83tncu6umpxq7egxmp
Is this okay? (y/n):y
2023/11/15 21:32:46 File /vagrant/secret.yaml synced with new keys
Таким образом можно управлять доступом пользователей к секретам.