1 parent
64be99308b
commit
5cba142003
38 files changed
+549
-337
No files matched your search
@@ -60,40 +60,59 @@ docker compose exec vault bao status # Sealed должно быть f
|
|||||||
|
|
||||||
Секреты из `.env` превращаются в переменные, которые ждут инструменты (`TF_VAR_*` для Terraform, `AWS_*` для backend), в секции `[env]` файла `mise.toml`.
|
Секреты из `.env` превращаются в переменные, которые ждут инструменты (`TF_VAR_*` для Terraform, `AWS_*` для backend), в секции `[env]` файла `mise.toml`.
|
||||||
|
|
||||||
## Как добавить сервис
|
## Бакеты, базы и сервисы
|
||||||
|
|
||||||
1. Описать его в `config.yaml`:
|
В `config.yaml` отдельно описано, что существует, и отдельно — кто имеет к этому доступ:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
services:
|
buckets:
|
||||||
orders:
|
|
||||||
postgres:
|
|
||||||
username: orders
|
|
||||||
name: orders
|
|
||||||
pgvector: true # необязательно, по умолчанию false
|
|
||||||
buckets:
|
|
||||||
orders-files:
|
orders-files:
|
||||||
versioning: true # по умолчанию true
|
versioning: true
|
||||||
noncurrent_days: 30 # сколько хранить старые версии
|
noncurrent_days: 30
|
||||||
```
|
reports:
|
||||||
|
versioning: false
|
||||||
|
expire_days: 90
|
||||||
|
|
||||||
Блоки `postgres` и `buckets` необязательны: сервису можно дать только базу или только бакеты.
|
databases:
|
||||||
|
orders:
|
||||||
|
pgvector: true
|
||||||
|
analytics: {}
|
||||||
|
|
||||||
2. Применить по очереди, читая `plan` перед каждым `apply`:
|
services:
|
||||||
|
orders:
|
||||||
|
buckets:
|
||||||
|
orders-files: rw
|
||||||
|
reports: ro
|
||||||
|
databases: [orders, analytics]
|
||||||
|
```
|
||||||
|
|
||||||
```bash
|
| Раздел | Поле | Значение |
|
||||||
mise run tf vault apply # политика и роль AppRole
|
|---|---|---|
|
||||||
mise run tf storage apply # бакеты и ключи S3
|
| `buckets` | `versioning` | хранить старые версии объектов, по умолчанию `true` |
|
||||||
mise run tf database apply # база, роль, пароли
|
| | `noncurrent_days` | сколько дней хранить старые версии, по умолчанию 30 |
|
||||||
```
|
| | `expire_days` | удалять объекты через столько дней; по умолчанию не удалять |
|
||||||
|
| `databases` | `pgvector` | установить расширение `vector`, по умолчанию `false` |
|
||||||
|
| `services` | `buckets` | бакет и доступ к нему: `rw` или `ro` |
|
||||||
|
| | `databases` | базы, в которых сервис работает |
|
||||||
|
|
||||||
3. Проверить: `mise run vault:app-check orders`.
|
Бакет или база без сервиса — это просто запись в `buckets` или `databases`. Один бакет и одну базу можно дать нескольким сервисам. Сервис — это учётная запись: для него создаются ключи S3, роли в Postgres, политика и роль AppRole в Vault.
|
||||||
|
|
||||||
Удаление сервиса — те же шаги в обратном порядке (`database`, `storage`, `vault`) после удаления блока из `config.yaml`. Бакет должен быть пустым. Переименование бакета или базы Terraform воспринимает как удаление старого и создание нового.
|
После правки применить по очереди, читая `plan` перед каждым `apply`:
|
||||||
|
|
||||||
Имена бакетов должны быть уникальны между сервисами; удобно начинать их с имени сервиса.
|
```bash
|
||||||
|
mise run tf vault apply # политики и роли AppRole
|
||||||
|
mise run tf storage apply # бакеты и ключи S3
|
||||||
|
mise run tf database apply # базы, роли, пароли
|
||||||
|
mise run vault:app-check # проверить доступы
|
||||||
|
```
|
||||||
|
|
||||||
Опечатка в имени необязательного поля (например, `noncurent_days`) ошибки не вызовет: лишнее поле молча отбрасывается, и берётся значение по умолчанию. После правки `config.yaml` смотрите в `plan`, что изменилось именно то, что вы хотели.
|
При удалении порядок обратный: `database`, `storage`, `vault`. Что стоит знать:
|
||||||
|
|
||||||
|
- Удалить можно только пустой бакет.
|
||||||
|
- Переименование бакета или базы Terraform воспринимает как удаление старого и создание нового.
|
||||||
|
- Ссылку сервиса на несуществующий бакет или базу и недопустимое значение доступа ловит проверка на `plan`.
|
||||||
|
- Опечатка в имени необязательного поля (например, `noncurent_days`) ошибки не вызовет: лишнее поле молча отбрасывается, и берётся значение по умолчанию. Смотрите в `plan`, что изменилось именно то, что вы хотели.
|
||||||
|
- Пока живы временные роли, выданные Vault, отобрать у сервиса базу не получится: Postgres ответит `dependent privileges exist`. Нужно дождаться истечения их срока (до часа) или отозвать токены сервиса.
|
||||||
|
|
||||||
## Что получает сервис
|
## Что получает сервис
|
||||||
|
|
||||||
@@ -102,8 +121,10 @@ docker compose exec vault bao status # Sealed должно быть f
|
|||||||
| Путь в Vault | Содержимое |
|
| Путь в Vault | Содержимое |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `secrets/<сервис>/s3` | `endpoint`, `buckets`, `access_key`, `secret_key` |
|
| `secrets/<сервис>/s3` | `endpoint`, `buckets`, `access_key`, `secret_key` |
|
||||||
| `secrets/<сервис>/postgres` | постоянные `host`, `port`, `database`, `username`, `password`, `sslmode` |
|
| `secrets/<сервис>/postgres/<база>` | постоянные `host`, `port`, `database`, `username`, `password`, `sslmode` |
|
||||||
| `database/creds/<сервис>` | временные `username` и `password`: Vault создаёт роль в Postgres на час и сам её удаляет |
|
| `database/creds/<сервис>-<база>` | временные `username` и `password`: Vault создаёт роль в Postgres на час и сам её удаляет |
|
||||||
|
|
||||||
|
В Postgres у сервиса отдельная роль на каждую базу, `<сервис>-<база>`. Владеет базой роль `<база>_owner` без права входа, а роли сервисов работают от её имени. Поэтому таблицы, созданные одним сервисом, доступны другому сервису той же базы.
|
||||||
|
|
||||||
Какой пароль базы выбрать: постоянный проще и подходит по умолчанию; временный не нужно ротировать и он не лежит в KV, но сервис обязан продлевать токен Vault, иначе через час потеряет доступ к базе.
|
Какой пароль базы выбрать: постоянный проще и подходит по умолчанию; временный не нужно ротировать и он не лежит в KV, но сервис обязан продлевать токен Vault, иначе через час потеряет доступ к базе.
|
||||||
|
|
||||||
@@ -123,7 +144,7 @@ curl -X POST -H "X-Vault-Token: $VAULT_TOKEN" \
|
|||||||
В `examples/go-service/` лежит небольшой сервис, который показывает всю цепочку: при запуске он знает только адрес Vault, `role_id` и `secret_id`, а ключи S3 и пароль базы получает из Vault.
|
В `examples/go-service/` лежит небольшой сервис, который показывает всю цепочку: при запуске он знает только адрес Vault, `role_id` и `secret_id`, а ключи S3 и пароль базы получает из Vault.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mise run example:up # первый сервис из config.yaml, постоянный пароль базы
|
mise run example:up # первый сервис с бакетом и базой, постоянный пароль
|
||||||
mise run example:up <сервис> dynamic # временная роль в Postgres от Vault
|
mise run example:up <сервис> dynamic # временная роль в Postgres от Vault
|
||||||
curl http://127.0.0.1:8090/ # состояние подключений
|
curl http://127.0.0.1:8090/ # состояние подключений
|
||||||
mise run example:down
|
mise run example:down
|
||||||
@@ -132,7 +153,7 @@ mise run example:down
|
|||||||
Задача `example:up` действует как администратор: берёт `role_id`, выпускает одноразовый `secret_id`, собирает образ и запускает контейнер в сети основного Compose. Дальше работает только код из `main.go`:
|
Задача `example:up` действует как администратор: берёт `role_id`, выпускает одноразовый `secret_id`, собирает образ и запускает контейнер в сети основного Compose. Дальше работает только код из `main.go`:
|
||||||
|
|
||||||
1. вход через AppRole клиентом `github.com/hashicorp/vault/api`;
|
1. вход через AppRole клиентом `github.com/hashicorp/vault/api`;
|
||||||
2. чтение `secrets/<сервис>/s3` и `secrets/<сервис>/postgres`;
|
2. чтение `secrets/<сервис>/s3` и `secrets/<сервис>/postgres/<база>`;
|
||||||
3. подключение к Postgres (`pgx`) и S3 (`minio-go`);
|
3. подключение к Postgres (`pgx`) и S3 (`minio-go`);
|
||||||
4. HTTP-обработчик, который показывает, под кем сервис работает в базе и сколько объектов видит в своих бакетах.
|
4. HTTP-обработчик, который показывает, под кем сервис работает в базе и сколько объектов видит в своих бакетах.
|
||||||
|
|
||||||
@@ -158,8 +179,10 @@ infra/
|
|||||||
├── data/ # данные сервисов, не в git
|
├── data/ # данные сервисов, не в git
|
||||||
├── modules/
|
├── modules/
|
||||||
│ ├── config/ # типы, проверки и значения по умолчанию для config.yaml
|
│ ├── config/ # типы, проверки и значения по умолчанию для config.yaml
|
||||||
│ ├── app-storage/ # бакеты, lifecycle, пользователь и ключи одного сервиса
|
│ ├── bucket/ # бакет, versioning и lifecycle
|
||||||
│ └── app-database/ # база, роль и права одного сервиса
|
│ ├── bucket-access/ # пользователь, политика и ключи S3 одного сервиса
|
||||||
|
│ ├── database/ # база, её роль-владелец и права
|
||||||
|
│ └── database-access/ # роль сервиса в одной базе
|
||||||
├── vault/ # Terraform: mount, политики, AppRole
|
├── vault/ # Terraform: mount, политики, AppRole
|
||||||
├── storage/ # Terraform: Silo и запись ключей в Vault
|
├── storage/ # Terraform: Silo и запись ключей в Vault
|
||||||
└── database/ # Terraform: Postgres, запись паролей и временные роли
|
└── database/ # Terraform: Postgres, запись паролей и временные роли
|
||||||
@@ -180,7 +203,7 @@ infra/
|
|||||||
|
|
||||||
Шаги проверены на чистой копии репозитория с пустым каталогом данных.
|
Шаги проверены на чистой копии репозитория с пустым каталогом данных.
|
||||||
|
|
||||||
0. Скопировать репозиторий без `.env`, `data/`, `vault_keys.txt` и каталогов `.terraform/` (в git их и так нет). В `config.yaml` заменить под свой проект: логины администраторов, порты, если стандартные заняты, имена бакетов для state и бэкапов, раздел `services`. Больше ничего править не нужно.
|
0. Скопировать репозиторий без `.env`, `data/`, `vault_keys.txt` и каталогов `.terraform/` (в git их и так нет). В `config.yaml` заменить под свой проект: логины администраторов, порты, если стандартные заняты, имя бакета для state, разделы `buckets`, `databases` и `services`. Бакет из `infra.silo.backup_bucket` должен быть описан в `buckets`. Больше ничего править не нужно.
|
||||||
1. Скопировать `.env.example` в `.env` и заполнить три пароля; `VAULT_TOKEN` оставить пустым, его запишет шаг 6. Спецсимволы в паролях ломают адреса подключения, поэтому удобно генерировать их командой `openssl rand -hex 32`. Затем `mise trust` и `mise run up`. Поднимать именно этой задачей: она выдаёт Vault и pgAdmin права на их каталоги данных, без чего на чистой машине они не запускаются.
|
1. Скопировать `.env.example` в `.env` и заполнить три пароля; `VAULT_TOKEN` оставить пустым, его запишет шаг 6. Спецсимволы в паролях ломают адреса подключения, поэтому удобно генерировать их командой `openssl rand -hex 32`. Затем `mise trust` и `mise run up`. Поднимать именно этой задачей: она выдаёт Vault и pgAdmin права на их каталоги данных, без чего на чистой машине они не запускаются.
|
||||||
2. Инициализировать Vault, сохранить unseal-ключ и root-токен, распечатать:
|
2. Инициализировать Vault, сохранить unseal-ключ и root-токен, распечатать:
|
||||||
|
|
||||||
@@ -222,13 +245,16 @@ infra/
|
|||||||
|
|
||||||
В коде нет комментариев, поэтому причины решений, которые не видны из самого кода, собраны здесь.
|
В коде нет комментариев, поэтому причины решений, которые не видны из самого кода, собраны здесь.
|
||||||
|
|
||||||
- **`depends_on` между грантами в `modules/app-database`.** Гранты `public` и `owner` меняют права одной и той же базы; без явного порядка Terraform выполнял бы их параллельно, и Postgres мог бы вернуть `tuple concurrently updated`.
|
- **`depends_on` между грантами в `modules/database`.** Гранты `public` и `owner` меняют права одной и той же базы; без явного порядка Terraform выполнял бы их параллельно, и Postgres мог бы вернуть `tuple concurrently updated`.
|
||||||
- **Грант `owner`.** После отзыва прав у `public` владелец базы не может подключиться к ней без явного `CONNECT`.
|
- **Грант `owner`.** После отзыва прав у `public` владелец базы не может подключиться к ней без явного `CONNECT`.
|
||||||
|
- **`assume_role` в `modules/database-access`.** Роль сервиса при входе переключается на владельца базы, поэтому всё, что она создаёт, принадлежит владельцу, а не ей самой. Иначе второй сервис той же базы не смог бы изменить чужие таблицы.
|
||||||
|
- **Отдельная роль на пару сервис и база.** `assume_role` задаётся для роли целиком, а владельцы у баз разные.
|
||||||
|
- **`depends_on` в output `owner` модуля `database`.** Роль сервиса не создаётся, пока владельцу не выданы права на базу.
|
||||||
- **`ignore_changes = [roles]` у роли `vault` в `database/main.tf`.** Членством управляет `postgresql_grant_role`; без этой строки два ресурса по очереди переписывают список ролей.
|
- **`ignore_changes = [roles]` у роли `vault` в `database/main.tf`.** Членством управляет `postgresql_grant_role`; без этой строки два ресурса по очереди переписывают список ролей.
|
||||||
- **`with_admin_option` там же.** Начиная с Postgres 16 роль с `CREATEROLE` добавляет участников только в те роли, где у неё есть `ADMIN OPTION`.
|
- **`with_admin_option` там же.** Начиная с Postgres 16 роль с `CREATEROLE` добавляет участников только в те роли, где у неё есть `ADMIN OPTION`.
|
||||||
- **`ALTER ROLE ... SET role` в `creation_statements`.** Временная роль работает от имени роли сервиса, поэтому созданные ею объекты принадлежат сервису, а саму её можно удалить без ошибок.
|
- **`ALTER ROLE ... SET role` в `creation_statements`.** Тот же приём для временных ролей Vault: созданные ими объекты принадлежат владельцу базы, а саму роль можно удалить без ошибок.
|
||||||
- **`delete_all_versions` у секретов.** Без него удалённый сервис оставлял бы в Vault восстановимые версии своих ключей.
|
- **`delete_all_versions` у секретов.** Без него удалённый сервис оставлял бы в Vault восстановимые версии своих ключей.
|
||||||
- **`prevent_destroy` у бакета со state.** В нём лежит state той самой конфигурации, которая им управляет.
|
- **`prevent_destroy` у бакета со state.** В нём лежит state той самой конфигурации, которая им управляет. Из-за этого он описан отдельным ресурсом, а не в `buckets`: `prevent_destroy` нельзя включить по условию.
|
||||||
- **Политика `terraform` в `vault/main.tf`.** Каждый путь взят из записи реальных запросов провайдера (`TF_LOG=DEBUG`) при добавлении и удалении сервиса. Создание и удаление mount, метода входа и самой политики в неё не входят намеренно.
|
- **Политика `terraform` в `vault/main.tf`.** Каждый путь взят из записи реальных запросов провайдера (`TF_LOG=DEBUG`) при добавлении и удалении сервиса. Создание и удаление mount, метода входа и самой политики в неё не входят намеренно.
|
||||||
- **`host` и `internal_host` в `config.yaml`.** Первый — адрес с хоста, им пользуется Terraform; второй — адрес внутри сети Compose, он попадает в секреты сервисов и в настройки Vault.
|
- **`host` и `internal_host` в `config.yaml`.** Первый — адрес с хоста, им пользуется Terraform; второй — адрес внутри сети Compose, он попадает в секреты сервисов и в настройки Vault.
|
||||||
- **`include` в `docker-compose.yml`.** Только так Compose читает второй env-файл для подстановки переменных; сам он загружает один `.env`.
|
- **`include` в `docker-compose.yml`.** Только так Compose читает второй env-файл для подстановки переменных; сам он загружает один `.env`.
|
||||||
|
|||||||
+19
-9
@@ -6,6 +6,7 @@ infra:
|
|||||||
port: 5455
|
port: 5455
|
||||||
internal_host: postgres
|
internal_host: postgres
|
||||||
internal_port: 5432
|
internal_port: 5432
|
||||||
|
sslmode: disable
|
||||||
pgadmin:
|
pgadmin:
|
||||||
image: dpage/pgadmin4:9.18.0
|
image: dpage/pgadmin4:9.18.0
|
||||||
port: 5050
|
port: 5050
|
||||||
@@ -27,17 +28,26 @@ infra:
|
|||||||
mount: secrets
|
mount: secrets
|
||||||
database_mount: database
|
database_mount: database
|
||||||
|
|
||||||
services:
|
buckets:
|
||||||
app:
|
|
||||||
postgres:
|
|
||||||
username: app
|
|
||||||
name: app
|
|
||||||
pgvector: true
|
|
||||||
buckets:
|
|
||||||
app-files:
|
app-files:
|
||||||
versioning: true
|
versioning: true
|
||||||
noncurrent_days: 30
|
noncurrent_days: 30
|
||||||
worker:
|
|
||||||
buckets:
|
|
||||||
worker-tmp:
|
worker-tmp:
|
||||||
versioning: false
|
versioning: false
|
||||||
|
infra-backups:
|
||||||
|
versioning: false
|
||||||
|
expire_days: 30
|
||||||
|
|
||||||
|
databases:
|
||||||
|
app:
|
||||||
|
pgvector: true
|
||||||
|
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
buckets:
|
||||||
|
app-files: rw
|
||||||
|
databases: [app]
|
||||||
|
worker:
|
||||||
|
buckets:
|
||||||
|
worker-tmp: rw
|
||||||
|
app-files: ro
|
||||||
+3
-5
@@ -5,9 +5,7 @@ locals {
|
|||||||
module "config" {
|
module "config" {
|
||||||
source = "../modules/config"
|
source = "../modules/config"
|
||||||
infra = local.config.infra
|
infra = local.config.infra
|
||||||
services = local.config.services
|
buckets = try(local.config.buckets, {})
|
||||||
}
|
databases = try(local.config.databases, {})
|
||||||
|
services = try(local.config.services, {})
|
||||||
locals {
|
|
||||||
databases = { for name, svc in module.config.services : name => svc.postgres if svc.postgres != null }
|
|
||||||
}
|
}
|
||||||
+28
-18
@@ -1,19 +1,31 @@
|
|||||||
module "app_database" {
|
module "database" {
|
||||||
source = "../modules/app-database"
|
source = "../modules/database"
|
||||||
for_each = local.databases
|
for_each = module.config.databases
|
||||||
service = { name = each.key, database = each.value }
|
name = each.key
|
||||||
|
owner = each.value.owner
|
||||||
|
pgvector = each.value.pgvector
|
||||||
|
}
|
||||||
|
|
||||||
|
module "database_access" {
|
||||||
|
source = "../modules/database-access"
|
||||||
|
for_each = module.config.service_databases
|
||||||
|
role = each.value.role
|
||||||
|
owner = module.database[each.value.database].owner
|
||||||
}
|
}
|
||||||
|
|
||||||
resource "vault_kv_secret_v2" "postgres" {
|
resource "vault_kv_secret_v2" "postgres" {
|
||||||
for_each = local.databases
|
for_each = module.config.service_databases
|
||||||
mount = module.config.vault.mount
|
mount = module.config.vault.mount
|
||||||
name = "${each.key}/postgres"
|
name = "${each.value.service}/postgres/${each.value.database}"
|
||||||
delete_all_versions = true
|
delete_all_versions = true
|
||||||
data_json = jsonencode(merge(module.app_database[each.key].credentials, {
|
data_json = jsonencode({
|
||||||
host = module.config.postgres.internal_host
|
host = module.config.postgres.internal_host
|
||||||
port = module.config.postgres.internal_port
|
port = module.config.postgres.internal_port
|
||||||
sslmode = each.value.sslmode
|
database = module.database[each.value.database].name
|
||||||
}))
|
username = module.database_access[each.key].username
|
||||||
|
password = module.database_access[each.key].password
|
||||||
|
sslmode = module.config.postgres.sslmode
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
resource "random_password" "vault" {
|
resource "random_password" "vault" {
|
||||||
@@ -33,12 +45,10 @@ resource "postgresql_role" "vault" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
resource "postgresql_grant_role" "vault" {
|
resource "postgresql_grant_role" "vault" {
|
||||||
for_each = local.databases
|
for_each = module.database
|
||||||
role = postgresql_role.vault.name
|
role = postgresql_role.vault.name
|
||||||
grant_role = each.value.username
|
grant_role = each.value.owner
|
||||||
with_admin_option = true
|
with_admin_option = true
|
||||||
|
|
||||||
depends_on = [module.app_database]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resource "vault_database_secrets_mount" "postgres" {
|
resource "vault_database_secrets_mount" "postgres" {
|
||||||
@@ -46,22 +56,22 @@ resource "vault_database_secrets_mount" "postgres" {
|
|||||||
|
|
||||||
postgresql {
|
postgresql {
|
||||||
name = "postgres"
|
name = "postgres"
|
||||||
connection_url = "postgresql://{{username}}:{{password}}@${module.config.postgres.internal_host}:${module.config.postgres.internal_port}/postgres?sslmode=disable"
|
connection_url = "postgresql://{{username}}:{{password}}@${module.config.postgres.internal_host}:${module.config.postgres.internal_port}/postgres?sslmode=${module.config.postgres.sslmode}"
|
||||||
username = postgresql_role.vault.name
|
username = postgresql_role.vault.name
|
||||||
password = random_password.vault.result
|
password = random_password.vault.result
|
||||||
allowed_roles = keys(local.databases)
|
allowed_roles = keys(module.config.service_databases)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
resource "vault_database_secret_backend_role" "service" {
|
resource "vault_database_secret_backend_role" "service" {
|
||||||
for_each = local.databases
|
for_each = module.config.service_databases
|
||||||
backend = vault_database_secrets_mount.postgres.path
|
backend = vault_database_secrets_mount.postgres.path
|
||||||
name = each.key
|
name = each.key
|
||||||
db_name = vault_database_secrets_mount.postgres.postgresql[0].name
|
db_name = vault_database_secrets_mount.postgres.postgresql[0].name
|
||||||
|
|
||||||
creation_statements = [
|
creation_statements = [
|
||||||
"CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}' IN ROLE \"${each.value.username}\";",
|
"CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}' IN ROLE \"${each.value.owner}\";",
|
||||||
"ALTER ROLE \"{{name}}\" SET role = \"${each.value.username}\";",
|
"ALTER ROLE \"{{name}}\" SET role = \"${each.value.owner}\";",
|
||||||
]
|
]
|
||||||
revocation_statements = [
|
revocation_statements = [
|
||||||
"DROP ROLE IF EXISTS \"{{name}}\";",
|
"DROP ROLE IF EXISTS \"{{name}}\";",
|
||||||
|
|||||||
@@ -8,6 +8,6 @@ provider "postgresql" {
|
|||||||
port = module.config.postgres.port
|
port = module.config.postgres.port
|
||||||
username = module.config.postgres.admin_user
|
username = module.config.postgres.admin_user
|
||||||
password = var.postgres_password
|
password = var.postgres_password
|
||||||
sslmode = "disable"
|
sslmode = module.config.postgres.sslmode
|
||||||
connect_timeout = 15
|
connect_timeout = 15
|
||||||
}
|
}
|
||||||
@@ -10,6 +10,7 @@ services:
|
|||||||
VAULT_KV_MOUNT: ${VAULT_MOUNT:-secrets}
|
VAULT_KV_MOUNT: ${VAULT_MOUNT:-secrets}
|
||||||
VAULT_DB_MOUNT: ${VAULT_DATABASE_MOUNT:-database}
|
VAULT_DB_MOUNT: ${VAULT_DATABASE_MOUNT:-database}
|
||||||
SERVICE_NAME: ${SERVICE_NAME:-}
|
SERVICE_NAME: ${SERVICE_NAME:-}
|
||||||
|
DATABASE_NAME: ${DATABASE_NAME:-}
|
||||||
POSTGRES_CREDS: ${POSTGRES_CREDS:-static}
|
POSTGRES_CREDS: ${POSTGRES_CREDS:-static}
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:${EXAMPLE_PORT:-8090}:8080"
|
- "127.0.0.1:${EXAMPLE_PORT:-8090}:8080"
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import (
|
|||||||
func main() {
|
func main() {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
service := mustEnv("SERVICE_NAME")
|
service := mustEnv("SERVICE_NAME")
|
||||||
|
database := mustEnv("DATABASE_NAME")
|
||||||
kvMount := mustEnv("VAULT_KV_MOUNT")
|
kvMount := mustEnv("VAULT_KV_MOUNT")
|
||||||
|
|
||||||
client, err := vault.NewClient(vault.DefaultConfig())
|
client, err := vault.NewClient(vault.DefaultConfig())
|
||||||
@@ -34,12 +35,12 @@ func main() {
|
|||||||
kv := client.KVv2(kvMount)
|
kv := client.KVv2(kvMount)
|
||||||
s3Secret, err := kv.Get(ctx, service+"/s3")
|
s3Secret, err := kv.Get(ctx, service+"/s3")
|
||||||
check(err, "секрет s3")
|
check(err, "секрет s3")
|
||||||
pgSecret, err := kv.Get(ctx, service+"/postgres")
|
pgSecret, err := kv.Get(ctx, service+"/postgres/"+database)
|
||||||
check(err, "секрет postgres")
|
check(err, "секрет postgres")
|
||||||
|
|
||||||
pgUser, pgPassword := str(pgSecret.Data, "username"), str(pgSecret.Data, "password")
|
pgUser, pgPassword := str(pgSecret.Data, "username"), str(pgSecret.Data, "password")
|
||||||
if os.Getenv("POSTGRES_CREDS") == "dynamic" {
|
if os.Getenv("POSTGRES_CREDS") == "dynamic" {
|
||||||
creds, err := client.Logical().ReadWithContext(ctx, mustEnv("VAULT_DB_MOUNT")+"/creds/"+service)
|
creds, err := client.Logical().ReadWithContext(ctx, mustEnv("VAULT_DB_MOUNT")+"/creds/"+service+"-"+database)
|
||||||
check(err, "временные учётные данные")
|
check(err, "временные учётные данные")
|
||||||
pgUser, pgPassword = str(creds.Data, "username"), str(creds.Data, "password")
|
pgUser, pgPassword = str(creds.Data, "username"), str(creds.Data, "password")
|
||||||
log.Printf("postgres: временная роль %s на %dс", pgUser, creds.LeaseDuration)
|
log.Printf("postgres: временная роль %s на %dс", pgUser, creds.LeaseDuration)
|
||||||
|
|||||||
@@ -2,18 +2,21 @@
|
|||||||
#MISE description="Запустить пример Go-сервиса: mise run example:up [сервис] [static|dynamic]"
|
#MISE description="Запустить пример Go-сервиса: mise run example:up [сервис] [static|dynamic]"
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
roles=$(terraform -chdir=vault output -json approle_role_ids)
|
config=$(echo 'jsonencode(yamldecode(file("config.yaml")))' | terraform console | jq -r .)
|
||||||
service=${1:-$(jq -r 'keys[0]' <<<"$roles")}
|
service=${1:-$(jq -r '[.services // {} | to_entries[] | select((.value.buckets // {} | length) > 0 and (.value.databases // [] | length) > 0) | .key][0] // empty' <<<"$config")}
|
||||||
|
[[ -n "$service" ]] || { echo "в config.yaml нет сервиса с бакетом и базой" >&2; exit 1; }
|
||||||
creds=${2:-static}
|
creds=${2:-static}
|
||||||
|
database=$(jq -r --arg s "$service" '.services[$s].databases[0] // empty' <<<"$config")
|
||||||
|
[[ -n "$database" ]] || { echo "у сервиса $service нет базы, пример рассчитан на сервис с базой и бакетом" >&2; exit 1; }
|
||||||
compose=(docker compose -f examples/go-service/compose.yml)
|
compose=(docker compose -f examples/go-service/compose.yml)
|
||||||
url=http://127.0.0.1:${EXAMPLE_PORT:-8090}/
|
url=http://127.0.0.1:${EXAMPLE_PORT:-8090}/
|
||||||
|
|
||||||
role_id=$(jq -er --arg s "$service" '.[$s]' <<<"$roles") ||
|
role_id=$(terraform -chdir=vault output -json approle_role_ids | jq -er --arg s "$service" '.[$s]') ||
|
||||||
{ echo "нет роли AppRole для сервиса $service" >&2; exit 1; }
|
{ echo "нет роли AppRole для сервиса $service" >&2; exit 1; }
|
||||||
secret_id=$(curl -sS -X POST -H "X-Vault-Token: $VAULT_TOKEN" -d '{"ttl": "5m", "num_uses": 1}' \
|
secret_id=$(curl -sS -X POST -H "X-Vault-Token: $VAULT_TOKEN" -d '{"ttl": "5m", "num_uses": 1}' \
|
||||||
"$VAULT_ADDR/v1/auth/approle/role/$service/secret-id" | jq -er .data.secret_id)
|
"$VAULT_ADDR/v1/auth/approle/role/$service/secret-id" | jq -er .data.secret_id)
|
||||||
|
|
||||||
export SERVICE_NAME=$service VAULT_ROLE_ID=$role_id VAULT_SECRET_ID=$secret_id POSTGRES_CREDS=$creds
|
export SERVICE_NAME=$service DATABASE_NAME=$database VAULT_ROLE_ID=$role_id VAULT_SECRET_ID=$secret_id POSTGRES_CREDS=$creds
|
||||||
INFRA_NETWORK=$(docker compose config --format json | jq -er .networks.infra.name)
|
INFRA_NETWORK=$(docker compose config --format json | jq -er .networks.infra.name)
|
||||||
export INFRA_NETWORK
|
export INFRA_NETWORK
|
||||||
"${compose[@]}" up -d --build --force-recreate --quiet-pull 2>&1 | tail -3
|
"${compose[@]}" up -d --build --force-recreate --quiet-pull 2>&1 | tail -3
|
||||||
|
|||||||
+84
-51
@@ -27,20 +27,40 @@ psql_as() {
|
|||||||
psql -h postgres -U "$1" -d "$3" -Atc "select session_user || ' работает как ' || current_user" 2>&1 | tail -1 || true
|
psql -h postgres -U "$1" -d "$3" -Atc "select session_user || ' работает как ' || current_user" 2>&1 | tail -1 || true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mc_cfg=/tmp/app-check-$$
|
||||||
|
mc_app() { docker compose exec -T object-storage mc --config-dir "$mc_cfg" "$@"; }
|
||||||
|
|
||||||
token=""
|
token=""
|
||||||
revoke_token() {
|
revoke_token() {
|
||||||
[[ -z "$token" ]] || vault_api POST auth/token/revoke-self "$token" >/dev/null 2>&1 || true
|
[[ -z "$token" ]] || vault_api POST auth/token/revoke-self "$token" >/dev/null 2>&1 || true
|
||||||
token=""
|
token=""
|
||||||
}
|
}
|
||||||
trap revoke_token EXIT
|
cleanup() {
|
||||||
|
revoke_token
|
||||||
|
docker compose exec -T object-storage rm -rf "$mc_cfg" >/dev/null 2>&1 || true
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
admin_token=$VAULT_TOKEN
|
admin_token=$VAULT_TOKEN
|
||||||
|
CONFIG=$(echo 'jsonencode(yamldecode(file("config.yaml")))' | terraform console | jq -r .)
|
||||||
ROLES=$(terraform -chdir=vault output -json approle_role_ids)
|
ROLES=$(terraform -chdir=vault output -json approle_role_ids)
|
||||||
mapfile -t ALL < <(jq -r 'keys[]' <<<"$ROLES")
|
mapfile -t ALL < <(jq -r '.services // {} | keys[]' <<<"$CONFIG")
|
||||||
|
mapfile -t ALL_BUCKETS < <(jq -r '.buckets // {} | keys[]' <<<"$CONFIG")
|
||||||
|
mapfile -t ALL_DBS < <(jq -r '.databases // {} | keys[]' <<<"$CONFIG")
|
||||||
if [[ $# -gt 0 ]]; then SERVICES=("$@"); else SERVICES=("${ALL[@]}"); fi
|
if [[ $# -gt 0 ]]; then SERVICES=("$@"); else SERVICES=("${ALL[@]}"); fi
|
||||||
|
|
||||||
|
contains() {
|
||||||
|
local needle=$1 item
|
||||||
|
shift
|
||||||
|
for item in "$@"; do [[ "$item" == "$needle" ]] && return 0; done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
for svc in "${SERVICES[@]}"; do
|
for svc in "${SERVICES[@]}"; do
|
||||||
printf '\n################ %s\n' "$svc"
|
printf '\n################ %s\n' "$svc"
|
||||||
|
mapfile -t buckets < <(jq -r --arg s "$svc" '.services[$s].buckets // {} | keys[]' <<<"$CONFIG")
|
||||||
|
mapfile -t ro_buckets < <(jq -r --arg s "$svc" '.services[$s].buckets // {} | to_entries[] | select(.value == "ro") | .key' <<<"$CONFIG")
|
||||||
|
mapfile -t dbs < <(jq -r --arg s "$svc" '.services[$s].databases // [] | .[]' <<<"$CONFIG")
|
||||||
|
|
||||||
role_id=$(jq -r --arg s "$svc" '.[$s] // empty' <<<"$ROLES")
|
role_id=$(jq -r --arg s "$svc" '.[$s] // empty' <<<"$ROLES")
|
||||||
[[ -n "$role_id" ]] || fail "нет роли AppRole для $svc"
|
[[ -n "$role_id" ]] || fail "нет роли AppRole для $svc"
|
||||||
@@ -53,83 +73,96 @@ for svc in "${SERVICES[@]}"; do
|
|||||||
token=$(body "$r" | jq -r .auth.client_token)
|
token=$(body "$r" | jq -r .auth.client_token)
|
||||||
ok "токен получен, политики: $(body "$r" | jq -r '.auth.policies | join(", ")'), ttl: $(body "$r" | jq -r .auth.lease_duration)s"
|
ok "токен получен, политики: $(body "$r" | jq -r '.auth.policies | join(", ")'), ttl: $(body "$r" | jq -r .auth.lease_duration)s"
|
||||||
|
|
||||||
step "2. Ключи S3 из $KV/$svc/s3"
|
step "2. S3: $KV/$svc/s3"
|
||||||
r=$(vault_api GET "$KV/data/$svc/s3" "$token")
|
r=$(vault_api GET "$KV/data/$svc/s3" "$token")
|
||||||
if [[ $(code "$r") == 200 ]]; then
|
if [[ ${#buckets[@]} -eq 0 ]]; then
|
||||||
|
[[ $(code "$r") == 404 ]] || fail "у сервиса нет бакетов, но секрет есть"
|
||||||
|
ok "бакетов нет, секрета нет"
|
||||||
|
else
|
||||||
|
[[ $(code "$r") == 200 ]] || fail "секрет не читается: HTTP $(code "$r")"
|
||||||
s3=$(body "$r" | jq .data.data)
|
s3=$(body "$r" | jq .data.data)
|
||||||
ok "поля: $(jq -r 'keys | join(", ")' <<<"$s3")"
|
ok "поля: $(jq -r 'keys | join(", ")' <<<"$s3")"
|
||||||
cfg=/tmp/app-check-$$
|
mc_app alias set app http://localhost:9000 "$(jq -r .access_key <<<"$s3")" "$(jq -r .secret_key <<<"$s3")" >/dev/null
|
||||||
docker compose exec -T object-storage mc --config-dir "$cfg" alias set app http://localhost:9000 \
|
for b in "${buckets[@]}"; do
|
||||||
"$(jq -r .access_key <<<"$s3")" "$(jq -r .secret_key <<<"$s3")" >/dev/null
|
mc_app ls "app/$b" >/dev/null 2>&1 || fail "свой бакет $b недоступен"
|
||||||
IFS=, read -ra own <<<"$(jq -r .buckets <<<"$s3")"
|
if contains "$b" "${ro_buckets[@]}"; then
|
||||||
for b in "${own[@]}"; do
|
out=$(echo check | mc_app pipe "app/$b/.app-check" 2>&1 || true)
|
||||||
docker compose exec -T object-storage mc --config-dir "$cfg" ls "app/$b" >/dev/null 2>&1 || fail "свой бакет $b недоступен"
|
[[ "$out" == *"Access Denied"* || "$out" == *"Insufficient permissions"* ]] ||
|
||||||
|
fail "запись в бакет $b с доступом ro прошла: $out"
|
||||||
|
ok "бакет $b: чтение есть, запись запрещена"
|
||||||
|
else
|
||||||
ok "бакет $b доступен"
|
ok "бакет $b доступен"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
for b in "${ALL_BUCKETS[@]}"; do
|
||||||
|
contains "$b" "${buckets[@]}" && continue
|
||||||
|
out=$(mc_app ls "app/$b" 2>&1 || true)
|
||||||
|
[[ "$out" == *"Access Denied"* ]] || fail "чужой бакет $b доступен: $out"
|
||||||
|
ok "чужой бакет $b: Access Denied"
|
||||||
done
|
done
|
||||||
docker compose exec -T object-storage rm -rf "$cfg"
|
|
||||||
else
|
|
||||||
ok "секрета нет (у сервиса нет бакетов), HTTP $(code "$r")"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
step "3. Постоянные учётные данные Postgres из $KV/$svc/postgres"
|
step "3. Postgres"
|
||||||
r=$(vault_api GET "$KV/data/$svc/postgres" "$token")
|
dyn_users=()
|
||||||
own_db=""
|
if [[ ${#dbs[@]} -eq 0 ]]; then
|
||||||
if [[ $(code "$r") == 200 ]]; then
|
ok "баз нет"
|
||||||
|
fi
|
||||||
|
for db in "${dbs[@]}"; do
|
||||||
|
r=$(vault_api GET "$KV/data/$svc/postgres/$db" "$token")
|
||||||
|
[[ $(code "$r") == 200 ]] || fail "секрет $KV/$svc/postgres/$db не читается: HTTP $(code "$r")"
|
||||||
pg=$(body "$r" | jq .data.data)
|
pg=$(body "$r" | jq .data.data)
|
||||||
own_db=$(jq -r .database <<<"$pg")
|
out=$(psql_as "$(jq -r .username <<<"$pg")" "$(jq -r .password <<<"$pg")" "$db")
|
||||||
ok "поля: $(jq -r 'keys | join(", ")' <<<"$pg")"
|
[[ "$out" == *"работает как ${db}_owner" ]] || fail "постоянная роль не подключилась к $db: $out"
|
||||||
out=$(psql_as "$(jq -r .username <<<"$pg")" "$(jq -r .password <<<"$pg")" "$own_db")
|
ok "$db, постоянная роль: $out"
|
||||||
[[ "$out" == *"работает как"* ]] || fail "не подключился к $own_db: $out"
|
|
||||||
ok "$own_db: $out"
|
|
||||||
else
|
|
||||||
ok "секрета нет (у сервиса нет базы), HTTP $(code "$r")"
|
|
||||||
fi
|
|
||||||
|
|
||||||
step "4. Временные учётные данные Postgres из $DB/creds/$svc"
|
r=$(vault_api GET "$DB/creds/$svc-$db" "$token")
|
||||||
if [[ -n "$own_db" ]]; then
|
[[ $(code "$r") == 200 ]] || fail "Vault не выдал временную роль для $db: $(body "$r")"
|
||||||
r=$(vault_api GET "$DB/creds/$svc" "$token")
|
|
||||||
[[ $(code "$r") == 200 ]] || fail "Vault не выдал временную роль: $(body "$r")"
|
|
||||||
dyn_user=$(body "$r" | jq -r .data.username)
|
dyn_user=$(body "$r" | jq -r .data.username)
|
||||||
dyn_pass=$(body "$r" | jq -r .data.password)
|
dyn_pass=$(body "$r" | jq -r .data.password)
|
||||||
ok "выдана роль $dyn_user на $(body "$r" | jq -r .lease_duration)s"
|
dyn_users+=("$dyn_user")
|
||||||
out=$(psql_as "$dyn_user" "$dyn_pass" "$own_db")
|
out=$(psql_as "$dyn_user" "$dyn_pass" "$db")
|
||||||
[[ "$out" == *"работает как"* ]] || fail "временная роль не подключилась к $own_db: $out"
|
[[ "$out" == *"работает как ${db}_owner" ]] || fail "временная роль не подключилась к $db: $out"
|
||||||
ok "$own_db: $out"
|
ok "$db, временная роль на $(body "$r" | jq -r .lease_duration)s: $out"
|
||||||
for other in $(docker compose exec -T postgres psql -U "$POSTGRES_USER" -d postgres -Atc \
|
|
||||||
"select datname from pg_database where datdba <> 10 and datname <> '$own_db'"); do
|
|
||||||
out=$(psql_as "$dyn_user" "$dyn_pass" "$other")
|
|
||||||
[[ "$out" == *"CONNECT privilege"* ]] || fail "временная роль попала в чужую базу $other: $out"
|
|
||||||
ok "$other: отказ, нет права CONNECT"
|
|
||||||
done
|
|
||||||
else
|
|
||||||
ok "пропущено, у сервиса нет базы"
|
|
||||||
fi
|
|
||||||
|
|
||||||
step "5. Чужие секреты"
|
for other in "${ALL_DBS[@]}"; do
|
||||||
for path in "$KV/data/infra/postgres" "$KV/data/infra/silo"; do
|
contains "$other" "${dbs[@]}" && continue
|
||||||
[[ $(code "$(vault_api GET "$path" "$token")") == 403 ]] || fail "сервис прочитал $path"
|
out=$(psql_as "$dyn_user" "$dyn_pass" "$other")
|
||||||
ok "$path: 403"
|
[[ "$out" == *"CONNECT privilege"* ]] || fail "роль попала в чужую базу $other: $out"
|
||||||
|
ok "чужая база $other: нет права CONNECT"
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
step "4. Чужие секреты"
|
||||||
|
for db in "${ALL_DBS[@]}"; do
|
||||||
|
contains "$db" "${dbs[@]}" && continue
|
||||||
|
[[ $(code "$(vault_api GET "$DB/creds/$svc-$db" "$token")") == 403 ]] || fail "сервис получил роль для чужой базы $db"
|
||||||
|
ok "$DB/creds/$svc-$db: 403"
|
||||||
done
|
done
|
||||||
for other in "${ALL[@]}"; do
|
for other in "${ALL[@]}"; do
|
||||||
[[ "$other" == "$svc" ]] && continue
|
[[ "$other" == "$svc" ]] && continue
|
||||||
for path in "$KV/data/$other/s3" "$KV/data/$other/postgres" "$DB/creds/$other"; do
|
paths=("$KV/data/$other/s3")
|
||||||
|
while read -r db; do
|
||||||
|
[[ -n "$db" ]] && paths+=("$KV/data/$other/postgres/$db" "$DB/creds/$other-$db")
|
||||||
|
done < <(jq -r --arg s "$other" '.services[$s].databases // [] | .[]' <<<"$CONFIG")
|
||||||
|
for path in "${paths[@]}"; do
|
||||||
[[ $(code "$(vault_api GET "$path" "$token")") == 403 ]] || fail "сервис прочитал $path"
|
[[ $(code "$(vault_api GET "$path" "$token")") == 403 ]] || fail "сервис прочитал $path"
|
||||||
ok "$path: 403"
|
ok "$path: 403"
|
||||||
done
|
done
|
||||||
done
|
done
|
||||||
|
|
||||||
step "6. Отзыв токена"
|
step "5. Отзыв токена"
|
||||||
old_token=$token
|
old_token=$token
|
||||||
revoke_token
|
revoke_token
|
||||||
[[ $(code "$(vault_api GET "$KV/data/$svc/s3" "$old_token")") == 403 ]] || fail "токен всё ещё работает"
|
[[ $(code "$(vault_api GET "$KV/data/$svc/s3" "$old_token")") == 403 ]] || fail "токен всё ещё работает"
|
||||||
ok "токен больше не принимается"
|
ok "токен больше не принимается"
|
||||||
if [[ -n "${dyn_user:-}" ]]; then
|
if [[ ${#dyn_users[@]} -gt 0 ]]; then
|
||||||
sleep 2
|
sleep 2
|
||||||
|
for dyn_user in "${dyn_users[@]}"; do
|
||||||
left=$(docker compose exec -T postgres psql -U "$POSTGRES_USER" -d postgres -Atc \
|
left=$(docker compose exec -T postgres psql -U "$POSTGRES_USER" -d postgres -Atc \
|
||||||
"select count(*) from pg_roles where rolname = '$dyn_user'")
|
"select count(*) from pg_roles where rolname = '$dyn_user'")
|
||||||
[[ "$left" == 0 ]] || fail "временная роль $dyn_user осталась в Postgres"
|
[[ "$left" == 0 ]] || fail "временная роль $dyn_user осталась в Postgres"
|
||||||
ok "временная роль $dyn_user удалена вместе с токеном"
|
done
|
||||||
dyn_user=""
|
ok "временные роли удалены вместе с токеном"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
locals {
|
|
||||||
databases = var.service.database == null ? {} : {
|
|
||||||
(var.service.database.name) = var.service.database
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
resource "random_password" "this" {
|
|
||||||
for_each = local.databases
|
|
||||||
length = 32
|
|
||||||
special = false
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "postgresql_role" "this" {
|
|
||||||
for_each = local.databases
|
|
||||||
name = each.value.username
|
|
||||||
login = true
|
|
||||||
password = random_password.this[each.key].result
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "postgresql_database" "this" {
|
|
||||||
for_each = local.databases
|
|
||||||
name = each.key
|
|
||||||
owner = postgresql_role.this[each.key].name
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "postgresql_extension" "vector" {
|
|
||||||
for_each = { for key, value in local.databases : key => value if value.pgvector }
|
|
||||||
name = "vector"
|
|
||||||
database = postgresql_database.this[each.key].name
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "postgresql_grant" "public" {
|
|
||||||
for_each = postgresql_database.this
|
|
||||||
database = each.value.name
|
|
||||||
role = "public"
|
|
||||||
object_type = "database"
|
|
||||||
privileges = []
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "postgresql_grant" "owner" {
|
|
||||||
for_each = postgresql_database.this
|
|
||||||
database = each.value.name
|
|
||||||
role = postgresql_role.this[each.key].name
|
|
||||||
object_type = "database"
|
|
||||||
privileges = ["CONNECT", "CREATE", "TEMPORARY"]
|
|
||||||
|
|
||||||
depends_on = [postgresql_grant.public]
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
output "credentials" {
|
|
||||||
value = one([for name, db in postgresql_database.this : {
|
|
||||||
database = db.name
|
|
||||||
username = postgresql_role.this[name].name
|
|
||||||
password = random_password.this[name].result
|
|
||||||
}])
|
|
||||||
description = "credentials of the service database, null if the service has no database"
|
|
||||||
sensitive = true
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
variable "service" {
|
|
||||||
type = object({
|
|
||||||
name = string
|
|
||||||
database = optional(object({
|
|
||||||
name = string
|
|
||||||
username = string
|
|
||||||
pgvector = optional(bool, false)
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
resource "minio_s3_bucket" "this" {
|
|
||||||
for_each = var.service.buckets
|
|
||||||
bucket = each.key
|
|
||||||
acl = "private"
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "minio_s3_bucket_versioning" "this" {
|
|
||||||
for_each = { for name, b in var.service.buckets : name => b if b.versioning }
|
|
||||||
bucket = minio_s3_bucket.this[each.key].bucket
|
|
||||||
|
|
||||||
versioning_configuration {
|
|
||||||
status = "Enabled"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "minio_ilm_policy" "this" {
|
|
||||||
for_each = var.service.buckets
|
|
||||||
bucket = minio_s3_bucket.this[each.key].bucket
|
|
||||||
|
|
||||||
rule {
|
|
||||||
id = "clean-rule-1"
|
|
||||||
status = "Enabled"
|
|
||||||
dynamic "noncurrent_expiration" {
|
|
||||||
for_each = each.value.versioning ? [1] : []
|
|
||||||
content {
|
|
||||||
days = "${each.value.noncurrent_days}d"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
expired_object_delete_marker = each.value.versioning
|
|
||||||
abort_incomplete_multipart_upload {
|
|
||||||
days_after_initiation = "3d"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "minio_iam_user" "this" {
|
|
||||||
name = var.service.name
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "minio_iam_policy" "get_upd_del_policy" {
|
|
||||||
name = var.service.name
|
|
||||||
policy = jsonencode({
|
|
||||||
Version = "2012-10-17"
|
|
||||||
Statement = [{
|
|
||||||
Effect = "Allow",
|
|
||||||
Action = ["s3:PutObject", "s3:GetObject", "s3:DeleteObject"],
|
|
||||||
Resource = [
|
|
||||||
for bucket in minio_s3_bucket.this : "arn:aws:s3:::${bucket.bucket}/*"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Effect = "Allow",
|
|
||||||
Action = ["s3:ListBucket"],
|
|
||||||
Resource = [
|
|
||||||
for bucket in minio_s3_bucket.this : "arn:aws:s3:::${bucket.bucket}"
|
|
||||||
]
|
|
||||||
}]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "minio_iam_user_policy_attachment" "this" {
|
|
||||||
user_name = minio_iam_user.this.name
|
|
||||||
policy_name = minio_iam_policy.get_upd_del_policy.name
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "minio_iam_service_account" "this" {
|
|
||||||
target_user = minio_iam_user.this.name
|
|
||||||
description = "keys of ${var.service.name}"
|
|
||||||
|
|
||||||
depends_on = [minio_iam_user_policy_attachment.this]
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
output "buckets" {
|
|
||||||
value = [for bucket in minio_s3_bucket.this : bucket.bucket]
|
|
||||||
description = "buckets of the service"
|
|
||||||
}
|
|
||||||
|
|
||||||
output "access_key" {
|
|
||||||
value = minio_iam_service_account.this.access_key
|
|
||||||
description = "access key of the service"
|
|
||||||
}
|
|
||||||
|
|
||||||
output "secret_key" {
|
|
||||||
value = minio_iam_service_account.this.secret_key
|
|
||||||
description = "secret key of the service"
|
|
||||||
sensitive = true
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
variable "service" {
|
|
||||||
type = object({
|
|
||||||
name = string
|
|
||||||
buckets = map(object({
|
|
||||||
versioning = optional(bool, true)
|
|
||||||
noncurrent_days = optional(number, 30)
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
validation {
|
|
||||||
condition = length(var.service.buckets) > 0
|
|
||||||
error_message = "У сервиса должен быть хотя бы один бакет: IAM-политика с пустым списком Resource невалидна."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
locals {
|
||||||
|
buckets = concat(var.read_write, var.read_only)
|
||||||
|
|
||||||
|
statements = [
|
||||||
|
{
|
||||||
|
Effect = "Allow"
|
||||||
|
Action = ["s3:PutObject", "s3:GetObject", "s3:DeleteObject"]
|
||||||
|
Resource = [for bucket in var.read_write : "arn:aws:s3:::${bucket}/*"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Effect = "Allow"
|
||||||
|
Action = ["s3:GetObject"]
|
||||||
|
Resource = [for bucket in var.read_only : "arn:aws:s3:::${bucket}/*"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Effect = "Allow"
|
||||||
|
Action = ["s3:ListBucket"]
|
||||||
|
Resource = [for bucket in local.buckets : "arn:aws:s3:::${bucket}"]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "minio_iam_user" "this" {
|
||||||
|
name = var.name
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "minio_iam_policy" "this" {
|
||||||
|
name = var.name
|
||||||
|
policy = jsonencode({
|
||||||
|
Version = "2012-10-17"
|
||||||
|
Statement = [for statement in local.statements : statement if length(statement.Resource) > 0]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "minio_iam_user_policy_attachment" "this" {
|
||||||
|
user_name = minio_iam_user.this.name
|
||||||
|
policy_name = minio_iam_policy.this.name
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "minio_iam_service_account" "this" {
|
||||||
|
target_user = minio_iam_user.this.name
|
||||||
|
description = "keys of ${var.name}"
|
||||||
|
|
||||||
|
depends_on = [minio_iam_user_policy_attachment.this]
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
output "buckets" {
|
||||||
|
value = local.buckets
|
||||||
|
}
|
||||||
|
|
||||||
|
output "access_key" {
|
||||||
|
value = minio_iam_service_account.this.access_key
|
||||||
|
}
|
||||||
|
|
||||||
|
output "secret_key" {
|
||||||
|
value = minio_iam_service_account.this.secret_key
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
variable "name" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "read_write" {
|
||||||
|
type = list(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "read_only" {
|
||||||
|
type = list(string)
|
||||||
|
}
|
||||||
File renamed without changes.
@@ -0,0 +1,33 @@
|
|||||||
|
resource "minio_s3_bucket" "this" {
|
||||||
|
bucket = var.name
|
||||||
|
acl = "private"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "minio_s3_bucket_versioning" "this" {
|
||||||
|
count = var.versioning ? 1 : 0
|
||||||
|
bucket = minio_s3_bucket.this.bucket
|
||||||
|
|
||||||
|
versioning_configuration {
|
||||||
|
status = "Enabled"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "minio_ilm_policy" "this" {
|
||||||
|
bucket = minio_s3_bucket.this.bucket
|
||||||
|
|
||||||
|
rule {
|
||||||
|
id = "cleanup"
|
||||||
|
status = "Enabled"
|
||||||
|
expiration = var.expire_days == null ? null : "${var.expire_days}d"
|
||||||
|
dynamic "noncurrent_expiration" {
|
||||||
|
for_each = var.versioning ? [1] : []
|
||||||
|
content {
|
||||||
|
days = "${var.noncurrent_days}d"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expired_object_delete_marker = var.versioning && var.expire_days == null
|
||||||
|
abort_incomplete_multipart_upload {
|
||||||
|
days_after_initiation = "3d"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
output "name" {
|
||||||
|
value = minio_s3_bucket.this.bucket
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
variable "name" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "versioning" {
|
||||||
|
type = bool
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "noncurrent_days" {
|
||||||
|
type = number
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "expire_days" {
|
||||||
|
type = number
|
||||||
|
default = null
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
minio = {
|
||||||
|
source = "aminueza/minio"
|
||||||
|
version = ">= 3.44.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,10 +2,42 @@ output "infra" {
|
|||||||
value = var.infra
|
value = var.infra
|
||||||
}
|
}
|
||||||
|
|
||||||
|
output "buckets" {
|
||||||
|
value = var.buckets
|
||||||
|
}
|
||||||
|
|
||||||
|
output "databases" {
|
||||||
|
value = {
|
||||||
|
for name, db in var.databases : name => merge(db, { owner = "${name}_owner" })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
output "services" {
|
output "services" {
|
||||||
value = var.services
|
value = var.services
|
||||||
}
|
}
|
||||||
|
|
||||||
|
output "service_buckets" {
|
||||||
|
value = {
|
||||||
|
for name, svc in var.services : name => {
|
||||||
|
read_write = [for bucket, access in svc.buckets : bucket if access == "rw"]
|
||||||
|
read_only = [for bucket, access in svc.buckets : bucket if access == "ro"]
|
||||||
|
} if length(svc.buckets) > 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output "service_databases" {
|
||||||
|
value = merge([
|
||||||
|
for name, svc in var.services : {
|
||||||
|
for db in svc.databases : "${name}-${db}" => {
|
||||||
|
service = name
|
||||||
|
database = db
|
||||||
|
role = "${name}-${db}"
|
||||||
|
owner = "${db}_owner"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]...)
|
||||||
|
}
|
||||||
|
|
||||||
output "vault" {
|
output "vault" {
|
||||||
value = {
|
value = {
|
||||||
address = "http://${var.infra.vault.host}:${var.infra.vault.port}"
|
address = "http://${var.infra.vault.host}:${var.infra.vault.port}"
|
||||||
|
|||||||
+66
-14
@@ -7,6 +7,7 @@ variable "infra" {
|
|||||||
port = number
|
port = number
|
||||||
internal_host = string
|
internal_host = string
|
||||||
internal_port = number
|
internal_port = number
|
||||||
|
sslmode = string
|
||||||
})
|
})
|
||||||
pgadmin = object({
|
pgadmin = object({
|
||||||
image = string
|
image = string
|
||||||
@@ -34,27 +35,78 @@ variable "infra" {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
variable "services" {
|
variable "buckets" {
|
||||||
type = map(object({
|
type = map(object({
|
||||||
postgres = optional(object({
|
|
||||||
username = string
|
|
||||||
name = string
|
|
||||||
sslmode = optional(string, "disable")
|
|
||||||
pgvector = optional(bool, false)
|
|
||||||
}))
|
|
||||||
buckets = optional(map(object({
|
|
||||||
versioning = optional(bool, true)
|
versioning = optional(bool, true)
|
||||||
noncurrent_days = optional(number, 30)
|
noncurrent_days = optional(number, 30)
|
||||||
})), {})
|
expire_days = optional(number)
|
||||||
}))
|
}))
|
||||||
|
default = {}
|
||||||
|
|
||||||
|
validation {
|
||||||
|
condition = alltrue([
|
||||||
|
for name in keys(var.buckets) : can(regex("^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$", name))
|
||||||
|
])
|
||||||
|
error_message = "Имя бакета: 3–63 символа, строчные латинские буквы, цифры, точки и дефисы."
|
||||||
|
}
|
||||||
|
|
||||||
|
validation {
|
||||||
|
condition = contains(keys(var.buckets), var.infra.silo.backup_bucket)
|
||||||
|
error_message = "Бакет из infra.silo.backup_bucket должен быть описан в buckets."
|
||||||
|
}
|
||||||
|
|
||||||
|
validation {
|
||||||
|
condition = !contains(keys(var.buckets), var.infra.silo.state_bucket)
|
||||||
|
error_message = "Бакет из infra.silo.state_bucket создаётся отдельно, в buckets его описывать нельзя."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "databases" {
|
||||||
|
type = map(object({
|
||||||
|
pgvector = optional(bool, false)
|
||||||
|
}))
|
||||||
|
default = {}
|
||||||
|
|
||||||
|
validation {
|
||||||
|
condition = alltrue([
|
||||||
|
for name in keys(var.databases) : can(regex("^[a-z_][a-z0-9_]{0,55}$", name))
|
||||||
|
])
|
||||||
|
error_message = "Имя базы: до 56 символов, строчные латинские буквы, цифры и подчёркивания, не с цифры."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "services" {
|
||||||
|
type = map(object({
|
||||||
|
buckets = optional(map(string), {})
|
||||||
|
databases = optional(list(string), [])
|
||||||
|
}))
|
||||||
|
default = {}
|
||||||
|
|
||||||
validation {
|
validation {
|
||||||
condition = alltrue(flatten([
|
condition = alltrue(flatten([
|
||||||
for svc in values(var.services) : [
|
for svc in values(var.services) : [for bucket in keys(svc.buckets) : contains(keys(var.buckets), bucket)]
|
||||||
for name in keys(svc.buckets) :
|
|
||||||
can(regex("^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$", name))
|
|
||||||
]
|
|
||||||
]))
|
]))
|
||||||
error_message = "Имя бакета: 3–63 символа, строчные латинские буквы, цифры, точки и дефисы."
|
error_message = "Сервис ссылается на бакет, которого нет в buckets."
|
||||||
|
}
|
||||||
|
|
||||||
|
validation {
|
||||||
|
condition = alltrue(flatten([
|
||||||
|
for svc in values(var.services) : [for access in values(svc.buckets) : contains(["rw", "ro"], access)]
|
||||||
|
]))
|
||||||
|
error_message = "Доступ к бакету: rw или ro."
|
||||||
|
}
|
||||||
|
|
||||||
|
validation {
|
||||||
|
condition = alltrue(flatten([
|
||||||
|
for svc in values(var.services) : [for db in svc.databases : contains(keys(var.databases), db)]
|
||||||
|
]))
|
||||||
|
error_message = "Сервис ссылается на базу, которой нет в databases."
|
||||||
|
}
|
||||||
|
|
||||||
|
validation {
|
||||||
|
condition = alltrue(flatten([
|
||||||
|
for name, svc in var.services : [for db in svc.databases : length("${name}-${db}") <= 63]
|
||||||
|
]))
|
||||||
|
error_message = "Имя роли <сервис>-<база> длиннее 63 символов."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
resource "random_password" "this" {
|
||||||
|
length = 32
|
||||||
|
special = false
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "postgresql_role" "this" {
|
||||||
|
name = var.role
|
||||||
|
login = true
|
||||||
|
password = random_password.this.result
|
||||||
|
roles = [var.owner]
|
||||||
|
assume_role = var.owner
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
output "username" {
|
||||||
|
value = postgresql_role.this.name
|
||||||
|
}
|
||||||
|
|
||||||
|
output "password" {
|
||||||
|
value = random_password.this.result
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
variable "role" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "owner" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
File renamed without changes.
@@ -0,0 +1,30 @@
|
|||||||
|
resource "postgresql_role" "owner" {
|
||||||
|
name = var.owner
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "postgresql_database" "this" {
|
||||||
|
name = var.name
|
||||||
|
owner = postgresql_role.owner.name
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "postgresql_extension" "vector" {
|
||||||
|
count = var.pgvector ? 1 : 0
|
||||||
|
name = "vector"
|
||||||
|
database = postgresql_database.this.name
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "postgresql_grant" "public" {
|
||||||
|
database = postgresql_database.this.name
|
||||||
|
role = "public"
|
||||||
|
object_type = "database"
|
||||||
|
privileges = []
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "postgresql_grant" "owner" {
|
||||||
|
database = postgresql_database.this.name
|
||||||
|
role = postgresql_role.owner.name
|
||||||
|
object_type = "database"
|
||||||
|
privileges = ["CONNECT", "CREATE", "TEMPORARY"]
|
||||||
|
|
||||||
|
depends_on = [postgresql_grant.public]
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
output "name" {
|
||||||
|
value = postgresql_database.this.name
|
||||||
|
}
|
||||||
|
|
||||||
|
output "owner" {
|
||||||
|
value = postgresql_role.owner.name
|
||||||
|
|
||||||
|
depends_on = [postgresql_grant.owner]
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
variable "name" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "owner" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "pgvector" {
|
||||||
|
type = bool
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
postgresql = {
|
||||||
|
source = "cyrilgdn/postgresql"
|
||||||
|
version = ">= 1.27.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
-5
@@ -5,9 +5,7 @@ locals {
|
|||||||
module "config" {
|
module "config" {
|
||||||
source = "../modules/config"
|
source = "../modules/config"
|
||||||
infra = local.config.infra
|
infra = local.config.infra
|
||||||
services = local.config.services
|
buckets = try(local.config.buckets, {})
|
||||||
}
|
databases = try(local.config.databases, {})
|
||||||
|
services = try(local.config.services, {})
|
||||||
locals {
|
|
||||||
services_with_buckets = { for name, svc in module.config.services : name => svc if length(svc.buckets) > 0 }
|
|
||||||
}
|
}
|
||||||
+14
-18
@@ -31,29 +31,25 @@ resource "minio_ilm_policy" "admin_policy" {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
resource "minio_s3_bucket" "backups" {
|
module "bucket" {
|
||||||
bucket = module.config.silo.backup_bucket
|
source = "../modules/bucket"
|
||||||
acl = "private"
|
for_each = module.config.buckets
|
||||||
|
name = each.key
|
||||||
|
versioning = each.value.versioning
|
||||||
|
noncurrent_days = each.value.noncurrent_days
|
||||||
|
expire_days = each.value.expire_days
|
||||||
}
|
}
|
||||||
|
|
||||||
resource "minio_ilm_policy" "backups" {
|
module "bucket_access" {
|
||||||
bucket = minio_s3_bucket.backups.bucket
|
source = "../modules/bucket-access"
|
||||||
|
for_each = module.config.service_buckets
|
||||||
rule {
|
name = each.key
|
||||||
id = "expire-old-backups"
|
read_write = [for bucket in each.value.read_write : module.bucket[bucket].name]
|
||||||
status = "Enabled"
|
read_only = [for bucket in each.value.read_only : module.bucket[bucket].name]
|
||||||
expiration = "30d"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module "app_storage" {
|
|
||||||
source = "../modules/app-storage"
|
|
||||||
for_each = local.services_with_buckets
|
|
||||||
service = { name = each.key, buckets = each.value.buckets }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resource "vault_kv_secret_v2" "s3" {
|
resource "vault_kv_secret_v2" "s3" {
|
||||||
for_each = module.app_storage
|
for_each = module.bucket_access
|
||||||
mount = module.config.vault.mount
|
mount = module.config.vault.mount
|
||||||
name = "${each.key}/s3"
|
name = "${each.key}/s3"
|
||||||
delete_all_versions = true
|
delete_all_versions = true
|
||||||
|
|||||||
+3
-1
@@ -5,5 +5,7 @@ locals {
|
|||||||
module "config" {
|
module "config" {
|
||||||
source = "../modules/config"
|
source = "../modules/config"
|
||||||
infra = local.config.infra
|
infra = local.config.infra
|
||||||
services = local.config.services
|
buckets = try(local.config.buckets, {})
|
||||||
|
databases = try(local.config.databases, {})
|
||||||
|
services = try(local.config.services, {})
|
||||||
}
|
}
|
||||||
+12
-10
@@ -10,14 +10,16 @@ resource "vault_policy" "service" {
|
|||||||
for_each = module.config.services
|
for_each = module.config.services
|
||||||
name = "service-${each.key}"
|
name = "service-${each.key}"
|
||||||
policy = jsonencode({
|
policy = jsonencode({
|
||||||
path = {
|
path = merge(
|
||||||
"${vault_mount.secret.path}/data/${each.key}/*" = {
|
{
|
||||||
capabilities = ["read"]
|
"${vault_mount.secret.path}/data/${each.key}/*" = { capabilities = ["read"] }
|
||||||
}
|
},
|
||||||
"${module.config.vault.database_mount}/creds/${each.key}" = {
|
{
|
||||||
capabilities = ["read"]
|
for key, access in module.config.service_databases :
|
||||||
}
|
"${module.config.vault.database_mount}/creds/${key}" => { capabilities = ["read"] }
|
||||||
}
|
if access.service == each.key
|
||||||
|
},
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,9 +61,9 @@ resource "vault_policy" "terraform" {
|
|||||||
"auth/${local.ar}/role/+/secret-id" = { capabilities = ["update"] }
|
"auth/${local.ar}/role/+/secret-id" = { capabilities = ["update"] }
|
||||||
|
|
||||||
"${local.kv}/data/+/s3" = { capabilities = local.manage }
|
"${local.kv}/data/+/s3" = { capabilities = local.manage }
|
||||||
"${local.kv}/data/+/postgres" = { capabilities = local.manage }
|
"${local.kv}/data/+/postgres/+" = { capabilities = local.manage }
|
||||||
"${local.kv}/metadata/+/s3" = { capabilities = ["read", "update", "delete"] }
|
"${local.kv}/metadata/+/s3" = { capabilities = ["read", "update", "delete"] }
|
||||||
"${local.kv}/metadata/+/postgres" = { capabilities = ["read", "update", "delete"] }
|
"${local.kv}/metadata/+/postgres/+" = { capabilities = ["read", "update", "delete"] }
|
||||||
|
|
||||||
"sys/mounts/${local.db}" = { capabilities = ["read"] }
|
"sys/mounts/${local.db}" = { capabilities = ["read"] }
|
||||||
"sys/mounts/${local.db}/tune" = { capabilities = ["update"] }
|
"sys/mounts/${local.db}/tune" = { capabilities = ["update"] }
|
||||||
|
|||||||
Reference in new issue
Block a user