Files
computer-playbook/roles/svc-db-postgres/tasks/01_init.yml
kevinveenbirkenbachandClaude Opus 5 b3ce43a596 fix(security): require no_log on every task that carries a credential
At -vvv Ansible prints each module argument and result, so a password in
a command line, a request body, a header or a fact lands in the deploy
log. Nothing enforced masking outside the MCP task files, and no check
looked at parents at all.

A new lint, test_credential_tasks_are_masked, finds tasks whose printed
parts (module arguments, environment, loop) reference an identifier ending
in password, secret, token, api_key or private_key, or
secrets.credentials. A task passes when it or an enclosing block sets
no_log, when every file that pulls it in does so through include_tasks
with apply.no_log or import_tasks with no_log (transitively), when the
secret only reaches a parameter the module masks itself, or when the
module masks its values. no_log on an include_tasks task itself does not
count, because it does not reach the included tasks. The rule can be
suppressed per task with `# nocheck: credential-masking`, now listed in
the suppression catalog, and the lint tests its parent and suppression
logic on synthetic cases.

It found 39 unmasked tasks in 19 roles, all real: join tokens, client
secrets, API tokens in Cloudflare headers, database passwords in
command lines, the ChatGPT bot login body and more. Each now sets
no_log from MASK_CREDENTIALS_IN_LOGS. The sys-dns-hetzner-rdns alias
hetzner_no_log, which only restated that switch, is gone.

Not covered yet: template and copy tasks whose rendered file carries a
secret print it as a diff under --diff.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 21:52:18 +02:00

159 lines
6.6 KiB
YAML

---
- name: "✅ Assert correct context"
ansible.builtin.assert:
that:
- application_id == 'svc-db-postgres'
- database_consumer_id != 'svc-db-postgres'
- name: 📐 Normalize postgres credential strings for module args
ansible.builtin.set_fact:
postgres_login_password_string: "{{ POSTGRES_PASSWORD | string }}"
postgres_database_password_string: "{{ database_password | string }}"
changed_when: false
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
- name: "🔍 Postgres | Resolve hosting node + container id"
ansible.builtin.include_tasks: "{{ lookup('path_absolute', 'roles/sys-svc-compose/tasks/utils/swarm/resolve_host_cid.yml') }}"
vars:
resolve_service_key: "{{ entity_name }}"
resolve_service_name: "{{ lookup('database', database_consumer_id, 'service_name') }}"
resolve_output_prefix: POSTGRES
resolve_compose_cid: "{{ lookup('database', database_consumer_id, 'container') }}"
- name: ⏳ Wait until postgres TCP auth works
block:
- name: ⏳ Wait for Postgres TCP auth
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
ansible.builtin.shell: > # nocheck: container-exec-resolver POSTGRES_LOCAL_CID resolved at delegate_to runtime
container exec -e PGPASSWORD={{ POSTGRES_PASSWORD | quote }} {{ POSTGRES_LOCAL_CID | quote }} sh -lc
'psql -h "{{ POSTGRES_LOCAL_HOST }}" -p "{{ POSTGRES_PORT }}"
-U postgres -d postgres -c "SELECT 1"'
delegate_to: "{{ POSTGRES_HOST_NODE }}"
register: pg_ready
until: pg_ready.rc == 0
retries: 30
delay: 5
changed_when: false
rescue:
- name: "💾 Postgres | Dump pg_hba.conf into the diagnostics folder" # nocheck: rescue-extra-diagnostics pg_hba.conf is postgres-specific auth config the generic rescue script cannot capture, and it is what a failed TCP-auth wait needs
ansible.builtin.script: "{{ lookup('path_absolute', 'roles/svc-db-postgres/files/shell/dump_pg_hba.sh') }}"
environment:
POSTGRES_CONTAINER_ADDRESS: "{{ POSTGRES_LOCAL_CID }}"
PGPASSWORD: "{{ POSTGRES_PASSWORD }}"
delegate_to: "{{ POSTGRES_HOST_NODE }}"
changed_when: false
failed_when: false
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
- name: "🛑 Postgres | fail after pg_hba dump"
ansible.builtin.fail:
msg: "Postgres TCP auth never became ready; pg_hba.conf was dumped into the diagnostics folder (the workflow-level rescue capture collects the full snapshot)"
- name: "📁 Create database: {{ database_name }}"
community.postgresql.postgresql_db:
name: "{{ database_name }}"
state: present
login_user: postgres
login_password: "{{ postgres_login_password_string }}"
login_host: "{{ POSTGRES_LOCAL_HOST }}"
login_port: "{{ POSTGRES_PORT }}"
register: postgresql_result
until: postgresql_result is succeeded
retries: "{{ POSTGRES_RETRIES }}"
delay: "{{ POSTGRES_DELAY }}"
- name: "📁 Create database user: {{ database_username }}"
community.postgresql.postgresql_user:
name: "{{ database_username }}"
password: "{{ postgres_database_password_string }}"
login_db: "{{ database_name }}"
state: present
login_user: postgres
login_password: "{{ postgres_login_password_string }}"
login_host: "{{ POSTGRES_LOCAL_HOST }}"
login_port: "{{ POSTGRES_PORT }}"
register: postgresql_result
until: postgresql_result is succeeded
retries: "{{ POSTGRES_RETRIES }}"
delay: "{{ POSTGRES_DELAY }}"
- name: "🟢 POSTGRES | Enable login for role {{ database_username }}"
# nocheck: database-query — svc-db-postgres bootstraps its own DB; the `database_query` module's `lookup('database', ...)` indirection isn't usable from inside the role that provides the lookup target.
community.postgresql.postgresql_query:
login_db: postgres
login_user: postgres
login_password: "{{ postgres_login_password_string }}"
login_host: "{{ POSTGRES_LOCAL_HOST }}"
login_port: "{{ POSTGRES_PORT }}"
query: |
ALTER ROLE "{{ database_username }}"
WITH LOGIN;
register: postgres_enable_login
until: postgres_enable_login is succeeded
retries: "{{ POSTGRES_RETRIES }}"
delay: "{{ POSTGRES_DELAY }}"
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
- name: "🔐 Grant ALL privileges on tables in public schema to {{ database_username }}"
community.postgresql.postgresql_privs:
login_db: "{{ database_name }}"
roles: "{{ database_username }}"
objs: ALL_IN_SCHEMA
privs: ALL
type: table
schema: public
state: present
login_user: postgres
login_password: "{{ postgres_login_password_string }}"
login_host: "{{ POSTGRES_LOCAL_HOST }}"
login_port: "{{ POSTGRES_PORT }}"
register: postgresql_result
until: postgresql_result is succeeded
retries: "{{ POSTGRES_RETRIES }}"
delay: "{{ POSTGRES_DELAY }}"
- name: "🔐 Grant all privileges on {{ database_name }} to {{ database_username }}"
community.postgresql.postgresql_privs:
login_db: "{{ database_name }}"
roles: "{{ database_username }}"
type: database
privs: ALL
state: present
login_user: postgres
login_password: "{{ postgres_login_password_string }}"
login_host: "{{ POSTGRES_LOCAL_HOST }}"
login_port: "{{ POSTGRES_PORT }}"
register: postgresql_result
until: postgresql_result is succeeded
retries: "{{ POSTGRES_RETRIES }}"
delay: "{{ POSTGRES_DELAY }}"
- name: "🔧 Set comprehensive schema privileges for {{ database_username }}"
# nocheck: database-query — svc-db-postgres bootstraps its own DB; the `database_query` module's `lookup('database', ...)` indirection isn't usable from inside the role that provides the lookup target.
community.postgresql.postgresql_query:
login_db: "{{ database_name }}"
login_user: postgres
login_password: "{{ postgres_login_password_string }}"
login_host: "{{ POSTGRES_LOCAL_HOST }}"
login_port: "{{ POSTGRES_PORT }}"
query: "{{ lookup('template', 'sql/grant_schema_privileges.sql.j2') }}"
register: postgresql_result
until: postgresql_result is succeeded
retries: "{{ POSTGRES_RETRIES }}"
delay: "{{ POSTGRES_DELAY }}"
- name: "✅ Ensure consumer-specific extensions ({{ database_consumer_id }})"
community.postgresql.postgresql_ext:
login_db: "{{ database_name }}"
ext: "{{ item }}"
state: present
login_user: postgres
login_password: "{{ postgres_login_password_string }}"
login_host: "{{ POSTGRES_LOCAL_HOST }}"
login_port: "{{ POSTGRES_PORT }}"
loop: "{{ lookup('service_extensions', 'postgres')[database_consumer_id] | default([]) }}"
register: postgresql_result
until: postgresql_result is succeeded
retries: "{{ POSTGRES_RETRIES }}"
delay: "{{ POSTGRES_DELAY }}"