Съдържание на курса
Vault от нулата (4): пароли, които се сменят сами
От Dorian Chávez · основател и главен изпълнителен директор на Hábil ·
Обновено на 21 октомври 2026 г.
Потребители на база данни, които се раждат, когато ги поискаш, и умират след две минути, пароли, които се ротират всяка минута, без никой да редактира файл, и програма на Python, която се свързва с PostgreSQL, без да знае нито една парола. И краят на загадката от 3:17.
Vault от нулата · сесия 4 от 4
Преговор на сесия 3
Отговори, без да гледаш. Отговорите са в решенията.
- Какво означават трите части на
vault:v2:SmfKDK1u...? - Ротира ключа. Какво става с данните, които вече са били шифровани, и коя операция ги качва към новата версия, без никой да види стойността?
- Защо един token, който може само да шифрова, е полезен за формуляр за регистрация?
Лабораторията на тази сесия е в публичното хранилище github.com/HabilMX/lab-vault. Първо я изгради сам; хранилището е за сравнение, когато нещо не ти излезе същото. Няма нито една вградена парола: лабораторията ги генерира при стартиране.
Цели на тази сесия
| # | Накрая можеш да… | Доказва се с |
|---|---|---|
| 1 | Настроиш механизма Database на Vault със специален потребител, без да ползваш суперпотребителя | Стъпки 2 и 3 |
| 2 | Поискаш динамични идентификационни данни, влезеш с psql и провериш, че спират да работят при изтичане или при отнемане |
Упражнения 16, 17 и 19 |
| 3 | Поставиш съществуващ потребител под автоматична ротация и докажеш, че старата парола вече не влиза | Упражнение 18 |
| 4 | Създадеш динамични идентификационни данни само за четене в MariaDB с клиента mariadb, провериш, че не могат да променят данни, и ги отнемеш |
Стъпка 10 |
| 5 | Изпълниш и промениш скрипт на Python, който иска идентификационните си данни от Vault, пита PostgreSQL и не пази никаква парола за база данни | Стъпка 12 и упражнение 20 |
Сценарий (измислица) — Глава 9: вторник, 9:00
Иван набира номер, който не е набирал от осем месеца.
— Тоньо? Иван е, от Canasta Morada. Слушай, един странен въпрос. Говори ли ти нещо сървър на име TR-REPORTES?
Мълчание. После смях.
— Не думай! Още ли е включен?
Тоньо разказва без свян. Преди две години и половина счетоводството поискало седмичен отчет за клиентите, които плащат с ваучери, с тяхното CURP, за да съгласува данните с фирмата за ваучерите. Базата данни била много натоварена през деня, затова Тоньо наел евтин сървър в облака, с картата на фирмата, и му сложил една програма:
- Всяка събота в 3:17 се свързвала с базата данни с паролата на
app_tienda. - Теглела резервното копие от нощта и
llave.txt, за да дешифрира CURP. - Сглобявала Excel файл и го пращала на имейла
reportes@.
— А като си тръгнах, забравих да ви кажа. Извинявай.
Росио проверява: пощенската кутия reportes@ има сто и седемнайсет непрочетени имейла. Никой не я е гледал, откакто предишният шеф на счетоводството си тръгнал.
Не е имало никакъв крадец.
Имало е нещо по-лошо: отворена врата, за чието съществуване никой не е знаел. Сто и седемнайсет копия на осем хиляди CURP, с ключа до тях, на сървър, който никой не е контролирал, в друга държава, плащан всеки месец с картата на фирмата.
А без журнал по самоличност и без идентификационни данни, които изтичат, било невъзможно да се различи от атака.
Глава 10: събота, 3:17. Един месец по-късно
Сървърът TR-REPORTES вече го няма; Иван го изтри заедно с Тоньо по видеоразговор, като гледаха заедно всяка папка.
Отчетът за ваучерите сега го прави нова програма, вътре в сървърите на Canasta Morada. В 3:17 тя иска от Vault идентификационни данни за базата данни. Vault създава потребител, който секунда по-рано не е съществувал, с право само за четене и с дата на изтичане. Програмата работи четири минути. В 3:21 потребителят вече не съществува.
Паролата на базата данни, с която Vault създава тези потребители, никой не я знае: нито Иван, нито доня Клара. Vault я смени още първия ден.
Телефонът на Иван не вибрира.
Валерия пише доклада за инцидента. В последния ред слага: „Никога няма да узнаем дали някой е видял публичното хранилище през онези часове. Затова сменихме паролата. Следващия път, когато някой качи .env по погрешка, там няма да е паролата на приложението, нито тази на Vault. Въпреки това онова, което съдържа този файл — като паролите за администриране — трябва да се смени незабавно.“
(Край на сценария.)
Защо е важно
В сесии 2 и 3 тайните вече не живееха във файлове, но си оставаха едни и същи с месеци. Ако някой прочетеше паролата на app_tienda във Vault, тя му вършеше работа, докато някой не я смени на ръка.
Днес правиш последната стъпка: пароли, които не съществуват, докато не ги поискаш, и които умират сами. Това е разликата между ключа от дома ти и пропуска за посетител на офис сграда. Ключът служи с години. Пропускът ти го дават на рецепцията на твое име, отваря само етажа, към който отиваш, а в 6 вечерта спира да работи. Ако го изгубиш в асансьора, никого не го е грижа.
Идеите на днес
Динамични идентификационни данни. Потребител на база данни, който Vault създава в момента, в който го поискаш, със случайна парола, и който изтрива, когато изтече. Всяка програма, и всеки път, когато поиска, получава различен. Ако изтече, върши работа минути.
Lease и TTL. Всички динамични идентификационни данни идват с lease (договор за ползване с краен срок). TTL (time to live, време на живот) е колко трае. Като паркинга на мола: билетът ти отбелязва часа на влизане и изтича; ако искаш повече време, плащаш пак (подновяваш) до някакъв максимум.
Отнемане (revoke). Анулиране на lease преди срока. Vault изтрива потребителя от базата данни в същия момент.
Статична роля с ротация. Има потребители, които не можеш да подмениш с нов всеки път, защото други системи ги познават по име. За тях Vault запазва потребителя и му сменя паролата през определен период, с rotation_period. Който има нужда от нея, я иска от Vault и винаги получава действащата (Database secrets engine).
Идентификационните данни на Vault в базата данни. За да създава и изтрива потребители, на Vault му е нужен собствен потребител в базата данни. Документацията предупреждава, че ако поискаш от Vault да ротира тази парола с rotate-root, след това никой няма да я знае, и препоръчва да се създаде специален потребител за Vault вместо да се ползва суперпотребителят (API на механизма Database). Това правим: потребител vault_admin с точно необходимите права, а не postgres, който може всичко.
Стъпка 1 — Подготви терминала
Лабораторията работи, печатът е отворен, token-ът е експортиран и променливите на PostgreSQL са заредени от .env:
docker compose start
set -a; source .env; set +a
export VAULT_ADDR=http://127.0.0.1:$PUERTO_VAULT
for i in 0 1 2; do
LLAVE=$(jq -r ".keys[$i]" secretos-locales/init.json)
curl -s -X PUT $VAULT_ADDR/v1/sys/unseal -d "{\"key\": \"$LLAVE\"}" | jq -c '{sealed, progress}'
done
export VAULT_TOKEN=$(jq -r .root_token secretos-locales/init.json)
export PGHOST=127.0.0.1 PGPORT=$PUERTO_POSTGRES PGDATABASE=tienda
Обърни внимание, че този път не експортираме PGUSER или PGPASSWORD: днес ще влизаш в PostgreSQL с няколко различни потребители и всяка команда ще казва кой.
Изходите в този урок са реални, но идват от независими изпълнения; затова часовете, имената на потребителите и номерата на версиите (като vault:v3:, което зависи от това колко пъти си ротирал ключа в сесия 3) не образуват една последователност. Твоите ще са различни.
Стъпка 2 — Потребител за Vault и един стар за ротация
Създай в PostgreSQL двама потребители, със случайно генерирана начална парола, която се записва в secretos-locales/ (git я пренебрегва):
vault_admin: онзи, който ще ползва Vault. Може да създава потребители (CREATEROLE) и да дава право за четене върху таблиците (WITH GRANT OPTION). Не е суперпотребител.reportes: потребител „от край време“, като онзи, който ползват отчетите. Ще го поставиш под автоматична ротация.
Всеки получава собствена начална парола: ако споделяха една и съща, който се докопа до тази на reportes, щеше да влезе като vault_admin, който може да създава потребители.
Контролна точка преди повтаряне. Тази стъпка очаква нова лаборатория. Ако я повториш, ще запишеш нови пароли във файловете на secretos-locales/, които вече не съвпадат с онези, които има PostgreSQL. Ако видиш role already exists или че механизмът database/ вече съществува, не генерирай отново началните файлове: провери състоянието и направи само стъпките, които липсват.
| Ако видиш | Значи | Какво да направиш |
|---|---|---|
CREATE ROLE или HTTP 204 |
Напреднал си добре | Продължи |
role "vault_admin" already exists |
Стъпка 2 вече е минала, изцяло или частично | Не генерирай наново началните файлове |
path is already in use |
Механизмът Database вече е монтиран | Прескочи към четене или настройка на връзката (стъпка 3) |
psql -v nombre=... дефинира променлива, която вътре се ползва като :'nombre'. Така паролата не остава буквално в командата, нито в историята ти. psql я замества, преди да изпрати SQL към PostgreSQL, така че паролата все пак пътува вътре в израза; затова извън лабораторията се ползва TLS и се избягва записването на изрази, които съдържат пароли:
$ VAULT_ADMIN_PASS=$(openssl rand -hex 16); echo "$VAULT_ADMIN_PASS" > secretos-locales/vault_admin_inicial.txt
$ REPORTES_PASS=$(openssl rand -hex 16); echo "$REPORTES_PASS" > secretos-locales/reportes_inicial.txt
$ PGUSER=postgres PGPASSWORD=$POSTGRES_PASSWORD psql -v vault_pass="$VAULT_ADMIN_PASS" -v reportes_pass="$REPORTES_PASS" <<SQL
CREATE ROLE vault_admin WITH LOGIN CREATEROLE PASSWORD :'vault_pass';
GRANT SELECT ON ALL TABLES IN SCHEMA public TO vault_admin WITH GRANT OPTION;
CREATE ROLE reportes WITH LOGIN PASSWORD :'reportes_pass';
GRANT SELECT ON clientes TO reportes;
GRANT reportes TO vault_admin WITH ADMIN OPTION;
SQL
CREATE ROLE
GRANT
CREATE ROLE
GRANT
GRANT ROLE
Последният ред дава на vault_admin право да администрира reportes, което му трябва, за да му смени паролата. От PostgreSQL 16 нататък потребител с CREATEROLE може да променя само потребителите, които е създал или които са му възложени по този начин.
Стъпка 3 — Свързване на Vault с PostgreSQL
Включи механизма Database:
$ curl -s -o /dev/null -w "HTTP %{http_code}\n" -X POST -H "X-Vault-Token: $VAULT_TOKEN" \
$VAULT_ADDR/v1/sys/mounts/database -d '{"type": "database"}'
HTTP 204
Сега връзката. Паролата на vault_admin я чете jq от файла с --arg: не я пишеш ти, не се появява в историята ти. В connection_url {{username}} и {{password}} ги попълва Vault, а сървърът се казва postgres, защото Vault го търси по име вътре в мрежата на Docker Compose, а не по 127.0.0.1. allowed_roles казва кои роли могат да ползват тази връзка:
$ jq -n --arg p "$(cat secretos-locales/vault_admin_inicial.txt)" '{
plugin_name: "postgresql-database-plugin",
connection_url: "postgresql://{{username}}:{{password}}@postgres:5432/tienda?sslmode=disable",
username: "vault_admin",
password: $p,
allowed_roles: ["lector", "reportes"]
}' | curl -s -w "HTTP %{http_code}\n" -X POST -H "X-Vault-Token: $VAULT_TOKEN" \
$VAULT_ADDR/v1/database/config/postgres -d @-
HTTP 204
Vault провери връзката, преди да я запази (verify_connection е включено по подразбиране); ако паролата беше грешна, тук щеше да ти каже. Прочети настройката:
$ curl -s -H "X-Vault-Token: $VAULT_TOKEN" $VAULT_ADDR/v1/database/config/postgres | jq .data
{
"allowed_roles": [
"lector",
"reportes"
],
"connection_details": {
"connection_url": "postgresql://{{username}}:{{password}}@postgres:5432/tienda?sslmode=disable",
"username": "vault_admin"
},
...
"plugin_name": "postgresql-database-plugin",
...
"verify_connection": true
}
Паролата не се появява. След като Vault я има, не я връща дори на root token-а.
Стъпка 4 — Роля за динамични идентификационни данни
Ролята е рецептата за създаване на потребител. Запази я в roles/lector-postgres.json. Този файл, politicas/app-python.hcl и python/clientes.py вече са в папката на лабораторията; ако предпочиташ да ги напишеш сам, създай папките с mkdir -p roles politicas python, отвори файла в редактора си, постави целия блок, без да го променяш, и го запази, преди да изпълниш следващата команда:
{
"db_name": "postgres",
"creation_statements": [
"CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}';",
"GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"{{name}}\";"
],
"revocation_statements": [
"REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA public FROM \"{{name}}\";",
"DROP ROLE IF EXISTS \"{{name}}\";"
],
"default_ttl": "2m",
"max_ttl": "10m"
}
creation_statements: SQL, който Vault изпълнява при създаването на потребителя.VALID UNTIL '{{expiration}}'кара самия PostgreSQL да отхвърли паролата при изтичане, дори Vault да се забави с изтриването. Това е примерът от документацията (PostgreSQL).revocation_statements: SQL за изтриването му. Защо го пишем на ръка, ще видиш в стъпка 6, с истинска грешка.default_ttlот 2 минути, за да видиш изтичането, без да чакаш; в реалния живот обикновено са минути или часове.max_ttl: дори да подновяваш, никога повече от 10.
Качи го. -d @archivo казва на curl да прати съдържанието на файла:
$ curl -s -o /dev/null -w "HTTP %{http_code}\n" -X POST -H "X-Vault-Token: $VAULT_TOKEN" \
$VAULT_ADDR/v1/database/roles/lector -d @roles/lector-postgres.json
HTTP 204
Стъпка 5 — Поискай идентификационни данни и влез
$ curl -s -H "X-Vault-Token: $VAULT_TOKEN" $VAULT_ADDR/v1/database/creds/lector > secretos-locales/creds-lector.json
$ jq "{lease_id, lease_duration, renewable, username: .data.username, password: (.data.password[0:6] + \"...\")}" secretos-locales/creds-lector.json
{
"lease_id": "database/creds/lector/rOpuliUuxFqc2UbdHMofQV8S",
"lease_duration": 120,
"renewable": true,
"username": "v-root-lector-rlOTkY9LU7LJR2xA22DB-1792335049",
"password": "INYcGz..."
}
Потребител, който секунда по-рано не е съществувал. Името носи кой го е поискал (root), ролята (lector), случайно парче и часа във формат Unix. В тази лаборатория root не е човек: това е просто token за администриране. В продукция не се дава на хора, нито на приложения; всеки човек или програма влиза със собствена самоличност (например OIDC, AppRole или Kubernetes), за да може името на временния потребител наистина да казва кой го е поискал. Влез с него:
$ export PGUSER=$(jq -r .data.username secretos-locales/creds-lector.json) PGPASSWORD=$(jq -r .data.password secretos-locales/creds-lector.json)
$ psql -c "SELECT current_user, nombre, left(curp, 9) FROM clientes;"
current_user | nombre | left
-----------------------------------------------+--------+-----------
v-root-lector-rlOTkY9LU7LJR2xA22DB-1792335049 | Pepe | vault:v3:
v-root-lector-rlOTkY9LU7LJR2xA22DB-1792335049 | María | vault:v3:
(2 rows)
Работи, и обърни внимание: дори със собствен потребител CURP си остават шифровани. Този четец може да види таблицата, но не и CURP; за това би му трябвало допълнително право в Transit. Опитай да изтриеш:
$ psql -c "DELETE FROM clientes;"
ERROR: permission denied for table clientes
Само за четене, както казва ролята. А от суперпотребителя можеш да видиш новия потребител с датата му на изтичане:
$ PGUSER=postgres PGPASSWORD=$POSTGRES_PASSWORD psql -c "SELECT rolname, rolvaliduntil FROM pg_roles WHERE rolname LIKE 'v-%';"
rolname | rolvaliduntil
-----------------------------------------------+------------------------
v-root-lector-rlOTkY9LU7LJR2xA22DB-1792335049 | 2026-10-18 14:52:54+00
(1 row)
Стъпка 6 — Отнемане… и истинска грешка
Бележка за четене. Първият блок възпроизвежда грешка от предишен опит. Не изпълнявай командите, които ползват secretos-locales/creds-lector-2.json: този файл не се създава в сегашния ход. В твоята лаборатория ролята от стъпка 4 вече носи revocation_statements; правилното отнемане го упражняваш ти в упражнение 17, с creds-16.json.
Когато за първи път изпробвахме този урок, ролята нямаше revocation_statements. Поискахме втори набор идентификационни данни и го отнехме преди срока със sys/leases/revoke:
$ jq -n --arg l "$(jq -r .lease_id secretos-locales/creds-lector-2.json)" "{lease_id: \$l, sync: true}" \
| curl -s -X PUT -H "X-Vault-Token: $VAULT_TOKEN" $VAULT_ADDR/v1/sys/leases/revoke -d @-
{"errors":["failed to revoke entry: resp: (*logical.Response)(nil) err: could not perform all revocation statements: ERROR: permission denied for sequence clientes_id_seq (SQLSTATE 42501)"]}
Код 400. Без revocation_statements Vault ползва SQL за изтриване по подразбиране на добавката за PostgreSQL, която се опитва да отнеме от потребителя правата върху всичко в схемата public, включително върху последователностите (броячите, които генерират id). Нашият vault_admin, с точно необходимите права, няма власт над последователността clientes_id_seq и PostgreSQL каза „не“. Временният потребител остана наполовина: без права върху таблицата, но неизтрит.
Добра грешка е: появява се защото не дадохме суперпотребителя на Vault. Правилното решение не е да му дадем повече власт, а да му кажем точно какво да прави. Затова ролята от стъпка 4 носи своите revocation_statements: да вземат онова, което е дадено, и да изтрият потребителя. С поправената роля, качена отново, същото отнемане работи:
$ jq -n --arg l "$(jq -r .lease_id secretos-locales/creds-lector-2.json)" "{lease_id: \$l, sync: true}" \
| curl -s -w "HTTP %{http_code}\n" -X PUT -H "X-Vault-Token: $VAULT_TOKEN" $VAULT_ADDR/v1/sys/leases/revoke -d @-
HTTP 204
$ PGUSER=$(jq -r .data.username secretos-locales/creds-lector-2.json) PGPASSWORD=$(jq -r .data.password secretos-locales/creds-lector-2.json) psql -At -c "SELECT count(*) FROM clientes;"
psql: error: connection to server at "127.0.0.1", port 5432 failed: FATAL: password authentication failed for user "v-root-lector-QyRyJ1vdjmrPY6U0KXl2-1792335063"
Потребителят вече не съществува. Ако си копирал ролята от стъпка 4 цялата, първото ти отнемане излиза вече както трябва.
"sync": true е важно: по подразбиране Vault само слага на опашка отнемането и отговаря 204 веднага, без да чака базата данни да изтрие потребителя. Със sync изчаква да приключи и така проверката ти дали можеш да влезеш веднага след това не зависи от късмета (sys/leases).
Стъпка 7 — Изчакване да изтече
Сега първите идентификационни данни, тези от стъпка 5. Не прави нищо: изчакай да минат двете им минути. Vault ги отнема сам и го записва в своя запис (docker compose logs vault):
2026-10-18T14:52:49.642Z [INFO] expiration: revoked lease: lease_id=database/creds/lector/rOpuliUuxFqc2UbdHMofQV8S
Опитай да влезеш:
$ date -u +%T; psql -At -c "SELECT count(*) FROM clientes;"
14:53:32
psql: error: connection to server at "127.0.0.1", port 5432 failed: FATAL: password authentication failed for user "v-root-lector-rlOTkY9LU7LJR2xA22DB-1792335049"
Никой не ги е отнел на ръка. Никой не е редактирал файл. Умряха сами. Това е онова, което би спасило Canasta Morada: паролата на скрипта на Тоньо би спряла да работи две минути след първата събота.
Стъпка 8 — Автоматична ротация на съществуващ потребител
reportes съществува отпреди и други системи го познават по име. Провери, че днес влиза с началната парола:
$ PGUSER=reportes PGPASSWORD=$(cat secretos-locales/reportes_inicial.txt) psql -At -c "SELECT current_user;"
reportes
Постави го под статична роля, която ротира всяка минута (минимумът, който API приема, е 5 секунди; една минута е, за да го видиш, без да заспиш):
$ curl -s -w "HTTP %{http_code}\n" -X POST -H "X-Vault-Token: $VAULT_TOKEN" $VAULT_ADDR/v1/database/static-roles/reportes \
-d '{"db_name": "postgres", "username": "reportes", "rotation_period": "1m"}'
HTTP 204
Документацията обяснява, че Vault ротира паролата при създаване на статичната роля, защото трябва да я знае (API на механизма Database). Така че началната вече е умряла:
$ curl -s -H "X-Vault-Token: $VAULT_TOKEN" $VAULT_ADDR/v1/database/static-creds/reportes \
| jq ".data | {username, password: (.password[0:6] + \"...\"), last_vault_rotation, ttl, rotation_period}"
{
"username": "reportes",
"password": "ZTU46F...",
"last_vault_rotation": "2026-10-18T14:51:57.637277774Z",
"ttl": 60,
"rotation_period": 60
}
$ PGUSER=reportes PGPASSWORD=$(cat secretos-locales/reportes_inicial.txt) psql -At -c "SELECT current_user;"
psql: error: connection to server at "127.0.0.1", port 5432 failed: FATAL: password authentication failed for user "reportes"
Който има нужда да влезе като reportes, иска от Vault действащата парола в момента:
$ PGUSER=reportes PGPASSWORD=$(curl -s -H "X-Vault-Token: $VAULT_TOKEN" $VAULT_ADDR/v1/database/static-creds/reportes | jq -r .data.password) \
psql -At -c "SELECT current_user, count(*) FROM clientes GROUP BY 1;"
reportes|2
Изчакай няколко минути и питай отново. last_vault_rotation е напреднало само, а журналът на Vault го потвърждава:
2026-10-18T14:52:59.517Z [INFO] secrets.database.database_4731c3d7: successfully rotated static role: name=reportes ttl=60
$ curl -s -H "X-Vault-Token: $VAULT_TOKEN" $VAULT_ADDR/v1/database/static-creds/reportes | jq -c ".data | {password: (.password[0:6] + \"...\"), last_vault_rotation, ttl}"
{"password":"OH0KOU...","last_vault_rotation":"2026-10-18T14:53:59.530969477Z","ttl":11}
А ако не искаш да чакаш, ротираш ти с rotate-role. Друга нова парола; тази от преди малко вече не влиза (проверяваш го в упражнение 18).
$ curl -s -o /dev/null -w "HTTP %{http_code}\n" -X POST -H "X-Vault-Token: $VAULT_TOKEN" $VAULT_ADDR/v1/database/rotate-role/reportes
HTTP 204
Ако вместо „всяка минута“ ти трябва „всяка неделя в 4 сутринта“, има rotation_schedule, във формат тип cron с пет полета; не може да се ползва заедно с rotation_period. Vault го тълкува в UTC, а не по часа на твоя град: за 4:00 сутринта в неделя в Мексико (UTC-6) се пише 0 10 * * SUN; нагласи сметката за твоята часова зона (API на механизма Database). В този урок не го изпробвахме.
Стъпка 9 — Никой да не знае паролата на Vault: rotate-root
Началната парола на vault_admin е в secretos-locales/vault_admin_inicial.txt. Днес работи:
$ PGUSER=vault_admin PGPASSWORD=$(cat secretos-locales/vault_admin_inicial.txt) psql -At -c "SELECT current_user;"
vault_admin
Поискай от Vault да я смени с такава, която знае само той:
$ curl -s -o /dev/null -w "HTTP %{http_code}\n" -X POST -H "X-Vault-Token: $VAULT_TOKEN" $VAULT_ADDR/v1/database/rotate-root/postgres
HTTP 204
$ PGUSER=vault_admin PGPASSWORD=$(cat secretos-locales/vault_admin_inicial.txt) psql -At -c "SELECT current_user;"
psql: error: connection to server at "127.0.0.1", port 5432 failed: FATAL: password authentication failed for user "vault_admin"
Файлът вече е боклук. А Vault продължава да работи, защото той знае новата:
$ curl -s -H "X-Vault-Token: $VAULT_TOKEN" $VAULT_ADDR/v1/database/creds/lector | jq -c "{username: .data.username, lease_duration}"
{"username":"v-root-lector-R3rwerQPR0yvfCxuAxYI-1792335221","lease_duration":120}
Сега никой човек не знае паролата, с която се създават потребителите на базата данни. Ако някой качи по погрешка конфигурационен файл, там вече няма тази на vault_admin. Внимавай с онова, което остава: .env още пази паролите за администриране на PostgreSQL и MariaDB на лабораторията, а rotate-root не ги сменя. В реалния живот и тези не бива да живеят във файл.
Стъпка 10 — Същото с MariaDB
MariaDB също работи от сесия 2. Паролата ѝ за root е в .env. Клиентът mariadb чете паролата от променливата MYSQL_PWD, така че и нея не я пишеш в командата. Създай таблица с продукти и потребителя vault_admin на MariaDB:
$ MARIA_ADMIN_PASS=$(openssl rand -hex 16); echo "$MARIA_ADMIN_PASS" > secretos-locales/vault_admin_maria_inicial.txt
$ MYSQL_PWD=$MARIADB_ROOT_PASSWORD mariadb -h 127.0.0.1 -P $PUERTO_MARIADB -u root tienda <<SQL
CREATE TABLE productos (id INT AUTO_INCREMENT PRIMARY KEY, nombre VARCHAR(50), precio DECIMAL(8,2));
INSERT INTO productos (nombre, precio) VALUES ('Cuaderno', 45.00), ('Pluma', 12.50);
CREATE USER 'vault_admin'@'%' IDENTIFIED BY '$MARIA_ADMIN_PASS';
GRANT CREATE USER ON *.* TO 'vault_admin'@'%';
GRANT SELECT ON tienda.* TO 'vault_admin'@'%' WITH GRANT OPTION;
SQL
WARNING: option --ssl-verify-server-cert is disabled, because of an insecure passwordless login.
Това предупреждение го дава новият клиент на MariaDB, когато се свързва, без да валидира самоличността на сървъра (TLS); в лабораторията няма значение. Ако ти пречи, добави --skip-ssl-verify-server-cert.
За MariaDB се ползва добавката mysql-database-plugin; документацията на Vault покрива MySQL и MariaDB на една и съща страница (MySQL/MariaDB). Обърни внимание, че адресът има друга форма:
$ jq -n --arg p "$(cat secretos-locales/vault_admin_maria_inicial.txt)" '{
plugin_name: "mysql-database-plugin",
connection_url: "{{username}}:{{password}}@tcp(mariadb:3306)/",
username: "vault_admin",
password: $p,
allowed_roles: ["lector-maria"]
}' | curl -s -w "HTTP %{http_code}\n" -X POST -H "X-Vault-Token: $VAULT_TOKEN" \
$VAULT_ADDR/v1/database/config/mariadb -d @-
HTTP 204
Ролята, в roles/lector-mariadb.json:
{
"db_name": "mariadb",
"creation_statements": [
"CREATE USER '{{name}}'@'%' IDENTIFIED BY '{{password}}';",
"GRANT SELECT ON tienda.* TO '{{name}}'@'%';"
],
"default_ttl": "2m",
"max_ttl": "10m"
}
$ curl -s -w "HTTP %{http_code}\n" -X POST -H "X-Vault-Token: $VAULT_TOKEN" $VAULT_ADDR/v1/database/roles/lector-maria -d @roles/lector-mariadb.json
HTTP 204
$ curl -s -H "X-Vault-Token: $VAULT_TOKEN" $VAULT_ADDR/v1/database/creds/lector-maria > secretos-locales/creds-maria.json
$ jq "{lease_id, lease_duration, username: .data.username, password: (.data.password[0:6] + \"...\")}" secretos-locales/creds-maria.json
{
"lease_id": "database/creds/lector-maria/SFcP2O5rDfL6qu6Bbt3RS0p6",
"lease_duration": 120,
"username": "v-root-lector-mar-24LwvJevZDrYbE",
"password": "VVeNO-..."
}
Тук не се наложи да пишем revocation_statements: изтриването по подразбиране на добавката за MySQL проработи с тези права. Влез, прочети и опитай да промениш цени:
$ MYSQL_PWD=$(jq -r .data.password secretos-locales/creds-maria.json) mariadb -h 127.0.0.1 -P $PUERTO_MARIADB \
-u $(jq -r .data.username secretos-locales/creds-maria.json) tienda -e "SELECT CURRENT_USER(), nombre, precio FROM productos;"
CURRENT_USER() nombre precio
v-root-lector-mar-24LwvJevZDrYbE@% Cuaderno 45.00
v-root-lector-mar-24LwvJevZDrYbE@% Pluma 12.50
$ MYSQL_PWD=$(jq -r .data.password secretos-locales/creds-maria.json) mariadb -h 127.0.0.1 -P $PUERTO_MARIADB \
-u $(jq -r .data.username secretos-locales/creds-maria.json) tienda -e "UPDATE productos SET precio = 0;"
ERROR 1142 (42000) at line 1: UPDATE command denied to user 'v-root-lector-mar-24LwvJevZDrYbE'@'172.21.0.1' for table `tienda`.`productos`
Отнеми и опитай отново:
$ jq -n --arg l "$(jq -r .lease_id secretos-locales/creds-maria.json)" "{lease_id: \$l, sync: true}" \
| curl -s -w "HTTP %{http_code}\n" -X PUT -H "X-Vault-Token: $VAULT_TOKEN" $VAULT_ADDR/v1/sys/leases/revoke -d @-
HTTP 204
$ MYSQL_PWD=$(jq -r .data.password secretos-locales/creds-maria.json) mariadb -h 127.0.0.1 -P $PUERTO_MARIADB \
-u $(jq -r .data.username secretos-locales/creds-maria.json) tienda -e "SELECT 1;"
ERROR 1045 (28000): Access denied for user 'v-root-lector-mar-24LwvJevZDrYbE'@'172.21.0.1' (using password: YES)
(Адресът 172.21.0.1 е на твоята машина, погледната от мрежата на Docker; твоят може да е друг.) И коренът:
$ curl -s -o /dev/null -w "HTTP %{http_code}\n" -X POST -H "X-Vault-Token: $VAULT_TOKEN" $VAULT_ADDR/v1/database/rotate-root/mariadb
HTTP 204
$ MYSQL_PWD=$(cat secretos-locales/vault_admin_maria_inicial.txt) mariadb --skip-ssl-verify-server-cert -h 127.0.0.1 -P $PUERTO_MARIADB -u vault_admin -e "SELECT CURRENT_USER();"
ERROR 1045 (28000): Access denied for user 'vault_admin'@'172.21.0.1' (using password: YES)
Стъпка 11 — Бутонът за паника
Помниш ли изискване R6 от сесия 1, отнемане наведнъж? Ако подозираш, че всички идентификационни данни на една роля са изтекли, ги анулираш заедно по префикс. С командата vault:
$ docker compose exec -e VAULT_TOKEN vault vault list sys/leases/lookup/database/creds/lector
Keys
----
HlDBUcqNtR5Ethbo2QU7T8P5
eKeykMcukrSwqRcDhqojSxlM
f9gqAm1x15HqsBwBTtYHHmm4
qDu1j4uI4WP6HP2LpIoyjBt5
$ docker compose exec -e VAULT_TOKEN vault vault lease revoke -prefix database/creds/lector
All revocation operations queued successfully!
$ docker compose exec -e VAULT_TOKEN vault vault list sys/leases/lookup/database/creds/lector
No value found at sys/leases/lookup/database/creds/lector
Внимавай с онова, което този изход показа: „queued“ значи, че Vault е приел заповедта и я е сложил на опашка, а празният списък казва само, че вече няма lease. Онова, което наистина има значение, е ефектът в PostgreSQL. Преброй временните потребители преди отнемането и няколко секунди след него:
PGUSER=postgres PGPASSWORD=$POSTGRES_PASSWORD psql -At -c "SELECT count(*) FROM pg_roles WHERE rolname LIKE 'v-root-lector-%';"
Преди отнемането трябва да даде повече от 0 (по един за всеки още жив набор от данни); след това — 0. Ако още не е 0, изчакай малко и повтори: опашката работи във фонов режим. Този брой потвърждава, че потребителите вече не съществуват.
Четири lease, анулирани с една заповед. Отнемането по префикс изисква специално право (sudo) на маршрута (sys/leases); затова тук го направихме с root token-а.
Стъпка 12 — Единствената програма: Python без пароли
Дотук всичко беше през терминала. Сега онова, което прави всяко истинско приложение: да поиска идентификационните си данни от Vault и да се свърже. Първо политика за програмата, politicas/app-python.hcl:
# La app de Python puede pedir usuarios temporales de solo lectura
# y descifrar CURPs. No puede cifrar, rotar ni leer otros secretos.
path "database/creds/lector" {
capabilities = ["read"]
}
path "transit/decrypt/curp" {
capabilities = ["update"]
}
Качи я и създай token от 30 минути за програмата:
$ jq -n --rawfile p politicas/app-python.hcl "{policy: \$p}" \
| curl -s -w "HTTP %{http_code}\n" -X PUT -H "X-Vault-Token: $VAULT_TOKEN" \
$VAULT_ADDR/v1/sys/policies/acl/app-python -d @-
HTTP 204
$ curl -s -H "X-Vault-Token: $VAULT_TOKEN" -X POST $VAULT_ADDR/v1/auth/token/create \
-d '{"policies": ["app-python"], "ttl": "30m", "display_name": "app-python"}' \
| jq -r .auth.client_token > secretos-locales/token-python.txt
Инсталирай конектора на PostgreSQL за Python във виртуална среда:
python3 -m venv .venv
.venv/bin/pip install "psycopg[binary]"
Днес не ти трябва да владееш целия Python. Приеми този файл като готова програма и гледай само четири зони: TOKEN чете тайна от средата; vault(...) говори с Vault; cred = ... иска временния потребител; а блокът with psycopg.connect(...) ползва този потребител в PostgreSQL. В упражнение 20 променяш само как завършва програмата.
И програмата, python/clientes.py. Ползва urllib, който идва с Python, за да говори с Vault, и psycopg за PostgreSQL. Те са 24 реда:
# Pide a Vault un usuario temporal de PostgreSQL, lee los clientes
# y descifra cada CURP con Transit. La app no guarda ninguna contraseña.
import base64, json, os, urllib.request
import psycopg
VAULT = os.environ.get("VAULT_ADDR", "http://127.0.0.1:8200")
TOKEN = os.environ["VAULT_TOKEN"] # te lo da quien opera Vault; nunca va en el código
def vault(ruta, datos=None):
cuerpo = json.dumps(datos).encode() if datos else None
req = urllib.request.Request(f"{VAULT}/v1/{ruta}", data=cuerpo,
headers={"X-Vault-Token": TOKEN})
with urllib.request.urlopen(req) as resp:
return json.load(resp)["data"]
cred = vault("database/creds/lector")
print("Vault me prestó el usuario", cred["username"])
with psycopg.connect(host="127.0.0.1", port=os.environ.get("PGPORT", "5432"),
dbname="tienda", user=cred["username"],
password=cred["password"]) as conexion:
for nombre, curp in conexion.execute("SELECT nombre, curp FROM clientes"):
claro = vault("transit/decrypt/curp", {"ciphertext": curp})["plaintext"]
print(f"{nombre:6} {curp[:12]}... -> {base64.b64decode(claro).decode()}")
Какво прави, по ред: функцията vault изпраща заявка към Vault с token-а в заглавната част (ако ѝ подадеш datos, е POST; ако не, GET) и връща частта data от отговора. Иска динамични идентификационни данни, свързва се с тях, чете клиентите и дешифрова всяко CURP с Transit.
Пусни я с token-а на програмата:
$ export VAULT_TOKEN=$(cat secretos-locales/token-python.txt)
$ .venv/bin/python python/clientes.py
Vault me prestó el usuario v-token-ap-lector-9FQ5IlE2i1HAUlZvuxZG-1792335175
Pepe vault:v3:VEk... -> PEPE000101HDFXXX01
María vault:v3:hKW... -> MARA990909MDFXXX02
Потърси във файла парола на PostgreSQL или на Transit: няма. Програмата наистина получава token на Vault от средата; този token също е тайна, но не е записан в кода, има ограничени права и трае само 30 минути. Името на потребителя вече започва с v-token-ap: Vault отбелязва, че го е поискал token app-python, а не root. Пусни я пак и ти заема друг потребител. А ако програмата се опита да излезе извън политиката си:
$ curl -s -w " HTTP %{http_code}\n" -H "X-Vault-Token: $VAULT_TOKEN" $VAULT_ADDR/v1/secret/data/tienda/postgres
{"errors":["1 error occurred:\n\t* permission denied\n\n"]}
HTTP 403
Без token дори не стартира:
$ unset VAULT_TOKEN; .venv/bin/python python/clientes.py
...
KeyError: 'VAULT_TOKEN'
Когато приключиш, върни се към root token-а: export VAULT_TOKEN=$(jq -r .root_token secretos-locales/init.json).
А от Java, .NET или Go?
Онова, което направи с
curlи Python, се прави от всеки език, защото Vault говори HTTP. Почти никой не го програмира на ръка: всеки език има библиотеки. HashiCorp поддържа списък, който разграничава официалните от тези на общността (Client libraries):
- Go:
github.com/hashicorp/vault/api, клиентът, който HashiCorp поддържа официално.- Java със Spring: Spring Cloud Vault зарежда тайните от Vault в конфигурацията на приложението и може да иска идентификационни данни за база данни в момента. HashiCorp го изброява сред библиотеките на общността.
- Java с Quarkus: разширението Quarkus Vault, от проекта Quarkiverse, покрива KV, Transit и динамични идентификационни данни за връзката с базата данни. И то е на общността.
- .NET: VaultSharp е най-известният вариант. Не е официален: това е проект на общността. Списъкът на HashiCorp споменава още един експериментален клиент за .NET, който според същата страница не е готов за продукция.
И има начин да не пипаш кода: Vault Agent, програма, която работи до приложението ти, легитимира се пред Vault, подновява своя token и записва тайните във файл по шаблон; приложението ти само чете този файл (Vault Agent). В Kubernetes инжекторът на Vault Agent го добавя сам към подовете, които го поискат с анотации, и оставя тайните в том в паметта, споделен с приложението (Agent Injector).
А как изглежда това в истинска система?
Представи си платформа, която обслужва много фирми едновременно и пази за всяка от тях паролите на нейните доставчици: достъпа до банката ѝ, до системата ѝ за фактури, до куриера ѝ. Ако една фирма види тайните на друга, бизнесът свършва.
Добрият дизайн не привързва програмата към Vault. Първо определя компонент с кратък контракт: „запечатай тази тайна за тази фирма“ и „отвори я“. Вътре всяка фирма има собствен ключ, с версии, като ключа на Transit, който ротира в сесия 3: ако един ключ се ротира, запечатаното преди още се отваря, а с
min_decryption_versionна старото се слага срок на годност.Зад този контракт се свързва доставчикът:
- за тестовете — версия, която пази всичко в паметта и се изтрива накрая;
- за продукцията — адаптер, който вика Transit с по един ключ за фирма (
transit/encrypt/empresa-a,transit/encrypt/empresa-b), или услуга за ключове на някой облак.Този доставчик може да е HashiCorp Vault или OpenBao, неговата свободна версия, която има съвместим механизъм Transit (Transit в OpenBao); в Hábil избрахме OpenBao.
Останалата част от системата никога не знае кой от двамата е свързан. Така може да се тества всичко, без да се стартира Vault, и да се сменя доставчикът, без да се пренаписва програмата. Обърни внимание на онова, което не прави: програмата никога не докосва ключа. Иска „запечатай“ и „отвори“, а ключът остава във Vault, както видя с
curlв сесия 3.
👉 Ако идваш от бизнес администрацията: динамичните идентификационни данни са пропускът за посетител, пренесен в системите: издава се на името на този, който го иска, отваря само необходимото и изтича сам. Автоматичната ротация е периодичната смяна на комбинацията на сейфа, но без никой да се налага да записва новата. А бутонът за паника отговаря на въпроса, който задава всеки одитор след инцидент: „колко време ви отне да анулирате достъпите?“. С това отговорът е „една команда“. В случая на Canasta Morada въпросът, който наистина имаше значение, беше друг: „кой плаща този сървър от 9,99 долара?“. Прегледът на повтарящите се такси по картата на фирмата също е контрол на достъпа.
Упражнения към сесия 4
Упражнение 16 — Твоят пропуск. Поискай идентификационни данни от ролята lector, влез с psql и преброй клиентите. После опитай INSERT INTO clientes (nombre, curp) VALUES ('Intruso', 'x');.
# Pista: guarda la credencial para usarla en el ejercicio 17.
# Archivo sugerido: secretos-locales/creds-16.json
# Ruta que debes consultar: database/creds/lector
Как проверяваш: броят дава 2 (или колкото имаш); INSERT дава ERROR: permission denied for table clientes.
Упражнение 17 — Отнеми ги сам. Отнеми идентификационните данни от упражнение 16 със sys/leases/revoke и опитай да влезеш отново. Направи го в рамките на 2 минути: ако Vault вече ги е отнел и маршрутът отговаря, че lease не съществува, това не е грешка в правата. Поискай други, замени creds-16.json и ги отнеми веднага.
Как проверяваш: отнемането отговаря HTTP 204, а psql отговаря password authentication failed. От суперпотребителя SELECT count(*) FROM pg_roles WHERE rolname = '<потребителят>'; дава 0.
Упражнение 18 — Паролата от преди минута. Запази действащата парола на reportes в променлива, ротирай с rotate-role/reportes и опитай да влезеш с онази, която си запазил.
Как проверяваш: със запазената — password authentication failed for user "reportes". С онази, която Vault ти дава след ротацията, влиза.
Упражнение 19 — Пропуск от 30 секунди. Смени default_ttl на ролята lector на 30 секунди, без да редактираш файла, с jq:
jq '.default_ttl = "30s"' roles/lector-postgres.json \
| curl -s -o /dev/null -w "HTTP %{http_code}\n" -X POST -H "X-Vault-Token: $VAULT_TOKEN" \
$VAULT_ADDR/v1/database/roles/lector -d @-
Поискай идентификационни данни, влез веднага, изчакай 40 секунди и опитай отново. Запази данните в secretos-locales/creds-19.json и сложи PGUSER=... PGPASSWORD=... psql пред всеки опит, като четеш стойностите от файла с jq както в стъпка 5: онова, което експортира в упражнения 16 и 17, принадлежи на вече отнет потребител и би ти дало неуспех, който не е от TTL.
Как проверяваш: lease_duration казва 30; първият опит влиза; вторият дава password authentication failed. Накрая върни ролята както си беше, като качиш отново roles/lector-postgres.json.
Упражнение 20 — Програмата да си прибира боклука. Всеки път, когато clientes.py работи, оставя жив временен потребител, докато изтече. Направи така, че да отнема собствения си token с auth/token/revoke-self и да го прави винаги, дори нещо да се провали по средата (ползвай try и finally). Когато един token се отнеме, Vault отнема и идентификационните данни, поискани с него. Тръгни от този скелет, който вече подрежда структурата:
try:
cred = vault("database/creds/lector")
# pega aquí el bloque with psycopg.connect(...) que ya existe
finally:
# aquí va la petición POST a auth/token/revoke-self
pass
Как проверяваш: преди да я пуснеш, преброй от суперпотребителя потребителите, които започват с v-token-ap; след като я пуснеш, броят трябва да остане същият като преди (потребителят е създаден и изтрит). Ако я пуснеш пак със същия token, трябва да се провали с HTTP Error 403, защото token-ът вече не съществува. За да повториш проверката след този 403, създай нов token със същата команда от стъпка 12, запази го отново в secretos-locales/token-python.txt и експортирай VAULT_TOKEN пак. 403 е очакваният само за вече отнетия token.
Често срещани грешки (и как да ги забележиш)
| Грешка | Как изглежда | Как да я забележиш | Какво да направиш |
|---|---|---|---|
Да ползваш 127.0.0.1 в connection_url |
При записване на конфигурацията: error verifying connection: ... dial tcp 127.0.0.1:5432: connect: connection refused, код 400 |
Сървърът в URL не е името на услугата; вътре в контейнера на Vault 127.0.0.1 е самият Vault |
В Docker Compose Vault стига до базата данни по име: postgres, mariadb |
Роля, която не е в allowed_roles |
"escritor" is not an allowed role, код 500 |
Името на ролята не се появява в database/config/<връзка> |
Добави я в allowed_roles на връзката |
| Отнемане, което се проваля при точно необходими права | permission denied for sequence ... при отнемане |
Ролята няма revocation_statements |
Напиши своите revocation_statements; не давай суперпотребител на Vault, за да изчезне грешката |
Да запазиш паролата на vault_admin и после да ротираш корена |
Файлът вече не върши работа | rotate-root отговори 204 |
Това искаме: никой човек не я знае. Ако ти трябва да влезеш, ползвай суперпотребителя на базата данни, а не този на Vault |
Да очакваш идентификационните данни да траят повече от max_ttl |
Подновяваш и все пак изтичат | lease_duration спира да расте |
max_ttl е таванът. Поискай нови данни |
| Да залепиш token-а на програмата в кода | Token-ът е записан във файл, който git следи | git status показва файла като променен, а grep -rn hvs. python/ намира token-а |
Token-ът влиза през променлива на средата или, още по-добре, с Vault Agent. Ако е качен, отнеми го |
psql влиза без да иска парола |
Мислиш, че данните още са живи | Свързал си се през локалния сокет вътре в контейнера | Свържи се като в урока, през мрежата (-h 127.0.0.1 от твоята машина), където PostgreSQL наистина проверява паролата |
Самооценка на серията
Отговори, без да гледаш. Ако сгрешиш повече от две, върни се към посочената сесия.
- Каква е разликата между
DELETEвърхуsecret/data/...иdestroy? (Сесия 2) - Напиши по памет политика, която позволява четене на
secret/data/app/*и нищо повече. (Сесия 2) - Защо 403 е добра новина, когато изпитваш политика? (Сесия 2)
- Какво ти трябва, за да дешифроваш
vault:v1:..., ако ключът имаmin_decryption_version: 2? (Сесия 3) - Какво печели една система с производни ключове и
context? (Сесия 3) - Каква е разликата между динамични идентификационни данни и статична роля? (Сесия 4)
- Защо връзката на Vault с базата данни ползва
vault_admin, а не суперпотребителя? (Сесия 4) - Един стажант напуска фирмата. С онова, което изгради, кои пароли би трябвало да смениш на ръка? (Всички)
Преговор на серията
| Сесия | Проблемът | Какво го решава | Командата, която не бива да забравяш |
|---|---|---|---|
| 1 | Тайни във файлове, в чатове, споделени и вечни | Изисквания R1 до R7 | git log -p -S <текст> |
| 2 | Никой не знае кой има достъп, нито кой го е ползвал | Печат на Шамир, token-и, политики, журнал | curl -H "X-Vault-Token: ..." $VAULT_ADDR/v1/secret/data/... |
| 3 | Ключът за шифроване живее до данните | Transit: шифроване, без да държиш ключа, ротация, rewrap | POST transit/encrypt/<ключ> с plaintext в base64 |
| 4 | Една и съща парола на базата данни за всички и завинаги | Динамични идентификационни данни, ротация, rotate-root, отнемане |
GET database/creds/<роля> |
За да изключиш всичко в края на серията: docker compose down. Данните ти остават в папките vault/, postgres/ и mariadb/. Ако искаш да започнеш отначало, изтрий тези папки, secretos-locales/ и .env и пусни пак ./iniciar.sh.
Решения (не ги гледай, преди да опиташ)
Преговор на сесия 3.
vault: шифровано от Vault;v2: с версия 2 на ключа; останалото: шифрованата стойност, в base64.- Остават шифровани със старата си версия и могат да се дешифроват, докато тази версия не падне под
min_decryption_version.rewrapги качва към новата версия, без да разкрие стойността. - Защото ако някой открадне този token, може да вкарва шифровани данни, но не може да прочете никакви. Вредата от един теч е много по-малка.
Упражнение 16.
curl -s -H "X-Vault-Token: $VAULT_TOKEN" $VAULT_ADDR/v1/database/creds/lector > secretos-locales/creds-16.json
export PGUSER=$(jq -r .data.username secretos-locales/creds-16.json) PGPASSWORD=$(jq -r .data.password secretos-locales/creds-16.json)
psql -At -c "SELECT count(*) FROM clientes;"
psql -c "INSERT INTO clientes (nombre, curp) VALUES ('Intruso', 'x');"
Упражнение 17. Командата от стъпка 6, с lease_id от creds-16.json. Направи го, преди да минат 2 минути; ако Vault те изпревари, поискай други данни и ги отнеми веднага.
Упражнение 18.
VIEJA=$(curl -s -H "X-Vault-Token: $VAULT_TOKEN" $VAULT_ADDR/v1/database/static-creds/reportes | jq -r .data.password)
curl -s -o /dev/null -w "HTTP %{http_code}\n" -X POST -H "X-Vault-Token: $VAULT_TOKEN" $VAULT_ADDR/v1/database/rotate-role/reportes
PGUSER=reportes PGPASSWORD=$VIEJA psql -At -c "SELECT 1;"
NUEVA=$(curl -s -H "X-Vault-Token: $VAULT_TOKEN" $VAULT_ADDR/v1/database/static-creds/reportes | jq -r .data.password)
PGUSER=reportes PGPASSWORD=$NUEVA psql -At -c "SELECT 1;"
В опита към този урок командата, която ползва $VIEJA (третата), даде FATAL: password authentication failed for user "reportes". Внимание: понеже ролята ротира всяка минута, „старата“ може да умре от автоматичната ротация преди тази от твоята ротация. Резултатът е същият. Последната команда, с $NUEVA, печата 1; ако и тя се провали, поискай отново от Vault действащата парола и я изпълни пак, защото ротацията от една минута може да е станала между две команди.
Упражнение 19.
curl -s -H "X-Vault-Token: $VAULT_TOKEN" $VAULT_ADDR/v1/database/creds/lector > secretos-locales/creds-19.json
PGUSER=$(jq -r .data.username secretos-locales/creds-19.json) \
PGPASSWORD=$(jq -r .data.password secretos-locales/creds-19.json) \
psql -At -c "SELECT count(*) FROM clientes;"
sleep 40
PGUSER=$(jq -r .data.username secretos-locales/creds-19.json) \
PGPASSWORD=$(jq -r .data.password secretos-locales/creds-19.json) \
psql -At -c "SELECT count(*) FROM clientes;"
В опита:
{"lease_duration":30,"username":"v-root-lector-wlrG6BL0dqUomIbQDDrV-1792336140"}
Първият опит върна 2. Четирийсет секунди по-късно:
psql: error: connection to server at "127.0.0.1", port 5432 failed: FATAL: password authentication failed for user "v-root-lector-wlrG6BL0dqUomIbQDDrV-1792336140"
Упражнение 20. Сложи вътре в try искането на идентификационните данни и всичко, което ползва базата данни, а отнемането във finally, който се изпълнява, каквото и да стане (дори да се провали самото искане на данните):
try:
cred = vault("database/creds/lector")
print("Vault me prestó el usuario", cred["username"])
with psycopg.connect(host="127.0.0.1", port=os.environ.get("PGPORT", "5432"),
dbname="tienda", user=cred["username"],
password=cred["password"]) as conexion:
for nombre, curp in conexion.execute("SELECT nombre, curp FROM clientes"):
claro = vault("transit/decrypt/curp", {"ciphertext": curp})["plaintext"]
print(f"{nombre:6} {curp[:12]}... -> {base64.b64decode(claro).decode()}")
finally:
vault_revoca = urllib.request.Request(f"{VAULT}/v1/auth/token/revoke-self", data=b"{}",
headers={"X-Vault-Token": TOKEN})
with urllib.request.urlopen(vault_revoca):
pass
print("Token revocado; el usuario temporal se va con él")
Така, ако заявката, връзката или някоя справка се провалят, token-ът се отнема въпреки това. data=b"{}" превръща заявката в POST, което иска този маршрут. В опита към този урок:
$ PGUSER=postgres PGPASSWORD=$POSTGRES_PASSWORD psql -At -c "SELECT count(*) FROM pg_roles WHERE rolname LIKE 'v-token-ap%';"
0
$ .venv/bin/python python/clientes.py
Vault me prestó el usuario v-token-ap-lector-xQMNqaIKMmkt9R54b5Du-1792336196
Pepe vault:v3:VEk... -> PEPE000101HDFXXX01
María vault:v3:hKW... -> MARA990909MDFXXX02
Token revocado; el usuario temporal se va con él
$ PGUSER=postgres PGPASSWORD=$POSTGRES_PASSWORD psql -At -c "SELECT count(*) FROM pg_roles WHERE rolname LIKE 'v-token-ap%';"
0
$ .venv/bin/python python/clientes.py
...
urllib.error.HTTPError: HTTP Error 403: Forbidden
Потребителят v-token-ap-lector-xQMN... се роди и умря в рамките на едно и също изпълнение. Програмата вече не оставя нищо след себе си.
Самооценка.
DELETEизтрива последната версия обратимо (undeleteя връща);destroyя изтрива завинаги.path "secret/data/app/*" { capabilities = ["read"] }- Защото доказва, че политиката забранява онова, което трябва да забранява. Политика, която никога не дава 403, вероятно дава повече от необходимото.
- Нищо не го дешифрова, докато минимумът е 2. Правилното беше да се обвие наново, преди да се вдигне минимумът; иначе трябва да се смъкне
min_decryption_version, да се обвие наново и пак да се вдигне. - Че шифровани данни за един собственик не могат да се дешифроват, сякаш са на друг, дори да се ползва същият ключ.
- Динамичните създават нов потребител всеки път и го изтриват при изтичане; статичната роля запазва потребител, който вече съществува, и му сменя паролата през определен период.
- Защото
rotate-rootоставя паролата в ръцете на Vault, а документацията препоръчва специален потребител. Освен това при точно необходими права грешка или теч във Vault не дава пълен контрол над базата данни. - Нито една не се сменя на ръка. Отнемаш неговия token във Vault и с него — динамичните му идентификационни данни (или всички, които е създал, по префикс). Едно изключение: ако е знаел паролата на статичната роля
reportes, ротирай я веднага сPOST /v1/database/rotate-role/reportes; отнемането на token-а му не променя тази парола, която би продължила да работи до следващата ротация. Това е разликата с Canasta Morada в началото на серията.
Какво следва
Тази серия използва Vault с един-единствен сървър, без TLS и с ключовете във файл, защото е лаборатория. Ако искаш да продължиш, следващите теми са: TLS на listener-а, методи за автентикация на машини (AppRole, Kubernetes) вместо token-и, създавани на ръка, съхранение с няколко сървъра за висока достъпност и резервни копия на самия Vault.
Пиша от онова, което виждаме всеки ден в Hábil, където изграждаме системи за банки, застрахователи и компании в Мексико. Ако в работата си или в семейния бизнес се сблъскате с такъв проблем, нека си поговорим: habil.mx
Ако във вашата фирма паролите на базите данни още живеят във файл, в Hábil изграждаме това като част от осигуряването на инфраструктурата: самоличност, ключове и тайни.
Предпочитате имейл? Пишете ни на hola@habil.mx