From b7ccca87f35ea4f084c73a105cf49d009f593b87 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Fri, 16 Jan 2026 08:06:29 -0800 Subject: [PATCH] Secure password management via 1Password CLI in ansible - All passwords fetched from 1Password at runtime using `op` CLI - pg_hba.conf uses scram-sha-256 everywhere (no trust mode) - initdb uses --pwfile for secure superuser password bootstrap - All password-handling tasks use no_log: true - Add borgmatic user with pg_read_all_data for backup dumps - Remove pg-setup mise task (no longer needed) - Miniflux fetches password directly from 1Password Requires: `op signin` before running ansible Co-Authored-By: Claude Opus 4.5 --- ansible/roles/miniflux/defaults/main.yml | 8 +- ansible/roles/miniflux/tasks/main.yml | 84 ++++-------- .../roles/miniflux/templates/miniflux.conf.j2 | 3 +- ansible/roles/postgresql/defaults/main.yml | 15 ++- ansible/roles/postgresql/tasks/main.yml | 125 +++++++++++++++--- .../roles/postgresql/templates/pg_hba.conf.j2 | 9 +- 6 files changed, 161 insertions(+), 83 deletions(-) diff --git a/ansible/roles/miniflux/defaults/main.yml b/ansible/roles/miniflux/defaults/main.yml index 68a8d58..5419803 100644 --- a/ansible/roles/miniflux/defaults/main.yml +++ b/ansible/roles/miniflux/defaults/main.yml @@ -5,21 +5,23 @@ miniflux_listen_addr: "127.0.0.1:8080" miniflux_base_url: "https://feed.tail8d86e.ts.net/" -# Database connection (password read from file on host) +# Database connection (password fetched from 1Password) miniflux_db_host: localhost miniflux_db_port: 5432 miniflux_db_name: miniflux miniflux_db_user: miniflux -miniflux_db_password_file: ~/.miniflux-db-password # Config paths miniflux_config_file: /opt/homebrew/etc/miniflux.conf +# 1Password settings for admin password +miniflux_op_vault: vg6xf6vvfmoh5hqjjhlhbeoaie +miniflux_op_item: ns6wylqiuqgczpo7gq2akaxbti + # First run settings # Set miniflux_create_admin to 1 for initial setup, then 0 after miniflux_create_admin: 0 miniflux_admin_username: admin -miniflux_admin_password_file: ~/.miniflux-admin-password # Always run migrations to keep schema updated miniflux_run_migrations: 1 diff --git a/ansible/roles/miniflux/tasks/main.yml b/ansible/roles/miniflux/tasks/main.yml index 99c6ca0..d5cdb62 100644 --- a/ansible/roles/miniflux/tasks/main.yml +++ b/ansible/roles/miniflux/tasks/main.yml @@ -1,78 +1,49 @@ --- # Miniflux installation and configuration # -# ONE-TIME SETUP (before running ansible): -# -# 1. Create miniflux database password in 1Password (same as PostgreSQL user password) -# 2. Create the password file on indri: -# -# ssh indri 'echo "your-password" > ~/.miniflux-db-password && chmod 600 ~/.miniflux-db-password' -# -# FIRST RUN (admin creation): -# -# 1. Create admin password in 1Password -# 2. Create admin password file on indri: -# -# ssh indri 'echo "your-admin-password" > ~/.miniflux-admin-password && chmod 600 ~/.miniflux-admin-password' -# -# 3. Set miniflux_create_admin: 1 in playbook vars or via --extra-vars -# 4. Run ansible: mise run provision-indri -- --tags miniflux -e miniflux_create_admin=1 -# 5. After successful first run, remove admin password file: -# -# ssh indri 'rm ~/.miniflux-admin-password' +# Prerequisites: +# - PostgreSQL role has run (creates database, user, and ~/.miniflux-db-password) +# - 1Password CLI authenticated on control machine # +# First run: +# mise run provision-indri -- --tags miniflux -e miniflux_create_admin=1 - name: Install miniflux via homebrew community.general.homebrew: name: miniflux state: present -- name: Check if database password file exists - ansible.builtin.stat: - path: "{{ miniflux_db_password_file }}" - register: db_password_file +# === Fetch passwords from 1Password === -- name: Fail if database password not configured - ansible.builtin.fail: - msg: | - Miniflux database password not found at {{ miniflux_db_password_file }} - Create it with: - echo "YOUR_PASSWORD" > {{ miniflux_db_password_file }} && chmod 600 {{ miniflux_db_password_file }} - Password should match what was set for the miniflux PostgreSQL user. - when: not db_password_file.stat.exists - -- name: Read database password - ansible.builtin.slurp: - src: "{{ miniflux_db_password_file }}" - register: db_password_content - when: db_password_file.stat.exists +- name: Fetch miniflux database password from 1Password + ansible.builtin.command: + cmd: op --vault {{ miniflux_op_vault }} item get {{ miniflux_op_item }} --fields password --reveal + delegate_to: localhost + register: miniflux_db_password_result + changed_when: false + no_log: true - name: Set database password fact ansible.builtin.set_fact: - miniflux_db_password: "{{ db_password_content.content | b64decode | trim }}" - when: db_password_file.stat.exists + miniflux_db_password: "{{ miniflux_db_password_result.stdout }}" + no_log: true -# Handle first run admin setup -- name: Check if admin password file exists (for first run) - ansible.builtin.stat: - path: "{{ miniflux_admin_password_file }}" - register: admin_password_file +- name: Fetch miniflux admin password from 1Password (for first run) + ansible.builtin.command: + cmd: op --vault {{ miniflux_op_vault }} item get {{ miniflux_op_item }} --fields admin-password --reveal + delegate_to: localhost + register: miniflux_admin_password_result + changed_when: false + no_log: true when: miniflux_create_admin | int == 1 -- name: Read admin password for first run - ansible.builtin.slurp: - src: "{{ miniflux_admin_password_file }}" - register: admin_password_content - when: - - miniflux_create_admin | int == 1 - - admin_password_file.stat.exists | default(false) - - name: Set admin password fact ansible.builtin.set_fact: - miniflux_admin_password: "{{ admin_password_content.content | b64decode | trim }}" - when: - - miniflux_create_admin | int == 1 - - admin_password_file.stat.exists | default(false) + miniflux_admin_password: "{{ miniflux_admin_password_result.stdout }}" + no_log: true + when: miniflux_create_admin | int == 1 + +# === Deploy configuration === - name: Deploy miniflux configuration ansible.builtin.template: @@ -80,6 +51,7 @@ dest: "{{ miniflux_config_file }}" mode: '0600' notify: restart miniflux + no_log: true - name: Ensure miniflux service is started ansible.builtin.command: brew services start miniflux diff --git a/ansible/roles/miniflux/templates/miniflux.conf.j2 b/ansible/roles/miniflux/templates/miniflux.conf.j2 index 2a479f4..2e23e07 100644 --- a/ansible/roles/miniflux/templates/miniflux.conf.j2 +++ b/ansible/roles/miniflux/templates/miniflux.conf.j2 @@ -1,5 +1,6 @@ # {{ ansible_managed }} # Miniflux configuration - KEY=VALUE format +# Passwords fetched from 1Password at deploy time. # Server settings LISTEN_ADDR={{ miniflux_listen_addr }} @@ -12,7 +13,7 @@ DATABASE_URL=postgres://{{ miniflux_db_user }}:{{ miniflux_db_password }}@{{ min RUN_MIGRATIONS={{ miniflux_run_migrations }} {% if miniflux_create_admin | int == 1 and miniflux_admin_password is defined %} -# First run admin creation +# First run admin creation (remove these after initial setup) CREATE_ADMIN=1 ADMIN_USERNAME={{ miniflux_admin_username }} ADMIN_PASSWORD={{ miniflux_admin_password }} diff --git a/ansible/roles/postgresql/defaults/main.yml b/ansible/roles/postgresql/defaults/main.yml index 89bd25a..eb18646 100644 --- a/ansible/roles/postgresql/defaults/main.yml +++ b/ansible/roles/postgresql/defaults/main.yml @@ -13,11 +13,24 @@ postgresql_config_dir: /opt/homebrew/var/postgresql@18 postgresql_port: 5432 postgresql_listen_addresses: "localhost" +# 1Password vault and item IDs for credentials +postgresql_op_vault: vg6xf6vvfmoh5hqjjhlhbeoaie +postgresql_op_superuser_item: guxu3j7ajhjyey6xxl2ovsl2ui +postgresql_op_miniflux_item: ns6wylqiuqgczpo7gq2akaxbti +postgresql_op_borgmatic_item: mw2bv5we7woicjza7hc6s44yvy + # Databases to create postgresql_databases: - name: miniflux owner: miniflux -# Users to create (passwords set manually via 1Password) +# Users to create (passwords fetched from 1Password) postgresql_users: - name: miniflux + op_item: "{{ postgresql_op_miniflux_item }}" + op_field: password + - name: borgmatic + op_item: "{{ postgresql_op_borgmatic_item }}" + op_field: db-password + roles: + - pg_read_all_data diff --git a/ansible/roles/postgresql/tasks/main.yml b/ansible/roles/postgresql/tasks/main.yml index 4471aae..c4e8658 100644 --- a/ansible/roles/postgresql/tasks/main.yml +++ b/ansible/roles/postgresql/tasks/main.yml @@ -1,35 +1,75 @@ --- # PostgreSQL installation and configuration # -# ONE-TIME SETUP (after running ansible): -# -# 1. Create the miniflux database password in 1Password -# 2. SSH to indri and set the password: -# -# /opt/homebrew/opt/postgresql@18/bin/psql -c "ALTER USER miniflux PASSWORD 'your-password-here';" -# -# 3. For borgmatic backups, create ~/.pgpass: -# echo "localhost:5432:miniflux:miniflux:YOUR_PASSWORD" > ~/.pgpass -# chmod 600 ~/.pgpass -# +# Passwords are fetched from 1Password at runtime using the `op` CLI. +# Requires: `op` authenticated on the control machine (run `op signin` first). - name: Install {{ postgresql_formula }} via homebrew community.general.homebrew: name: "{{ postgresql_formula }}" state: present +# === Fetch passwords from 1Password (on control machine) === + +- name: Fetch superuser password from 1Password + ansible.builtin.command: + cmd: op --vault {{ postgresql_op_vault }} item get {{ postgresql_op_superuser_item }} --fields password --reveal + delegate_to: localhost + register: pg_superuser_password_result + changed_when: false + no_log: true + +- name: Set superuser password fact + ansible.builtin.set_fact: + pg_superuser_password: "{{ pg_superuser_password_result.stdout }}" + no_log: true + +- name: Fetch user passwords from 1Password + ansible.builtin.command: + cmd: op --vault {{ postgresql_op_vault }} item get {{ item.op_item }} --fields {{ item.op_field }} --reveal + delegate_to: localhost + loop: "{{ postgresql_users }}" + register: pg_user_passwords_result + changed_when: false + no_log: true + +- name: Build user password lookup + ansible.builtin.set_fact: + pg_user_passwords: "{{ pg_user_passwords | default({}) | combine({item.item.name: item.stdout}) }}" + loop: "{{ pg_user_passwords_result.results }}" + no_log: true + +# === Initialize PostgreSQL cluster === + - name: Check if postgresql data directory is initialized ansible.builtin.stat: path: "{{ postgresql_data_dir }}/PG_VERSION" register: pg_data -- name: Initialize postgresql database cluster +- name: Create temporary password file for initdb + ansible.builtin.copy: + content: "{{ pg_superuser_password }}" + dest: /tmp/.pg_init_pwfile + mode: '0600' + when: not pg_data.stat.exists + no_log: true + +- name: Initialize postgresql database cluster with superuser password ansible.builtin.command: > {{ postgresql_bin_dir }}/initdb --locale=en_US.UTF-8 -E UTF-8 + --pwfile=/tmp/.pg_init_pwfile {{ postgresql_data_dir }} when: not pg_data.stat.exists +- name: Remove temporary password file + ansible.builtin.file: + path: /tmp/.pg_init_pwfile + state: absent + when: not pg_data.stat.exists + +# === Configure and start PostgreSQL === + - name: Deploy pg_hba.conf ansible.builtin.template: src: pg_hba.conf.j2 @@ -52,38 +92,85 @@ delay: 2 changed_when: false -# Create database users (without passwords - set manually via 1Password) +# === Create users with passwords === + - name: Check if postgresql users exist ansible.builtin.command: > - {{ postgresql_bin_dir }}/psql -tAc + {{ postgresql_bin_dir }}/psql -h localhost -tAc "SELECT 1 FROM pg_roles WHERE rolname = '{{ item.name }}';" + environment: + PGPASSWORD: "{{ pg_superuser_password }}" loop: "{{ postgresql_users }}" register: user_check changed_when: false failed_when: false + no_log: true -- name: Create postgresql users +- name: Create postgresql users with passwords ansible.builtin.command: > - {{ postgresql_bin_dir }}/psql -c "CREATE USER {{ item.item.name }};" + {{ postgresql_bin_dir }}/psql -h localhost -c + "CREATE USER {{ item.item.name }} WITH PASSWORD '{{ pg_user_passwords[item.item.name] }}';" + environment: + PGPASSWORD: "{{ pg_superuser_password }}" loop: "{{ user_check.results }}" when: item.stdout != "1" changed_when: true + no_log: true + +- name: Update postgresql user passwords (idempotent) + ansible.builtin.command: > + {{ postgresql_bin_dir }}/psql -h localhost -c + "ALTER USER {{ item.name }} WITH PASSWORD '{{ pg_user_passwords[item.name] }}';" + environment: + PGPASSWORD: "{{ pg_superuser_password }}" + loop: "{{ postgresql_users }}" + changed_when: false + no_log: true + +# === Grant roles to users === + +- name: Grant roles to users + ansible.builtin.command: > + {{ postgresql_bin_dir }}/psql -h localhost -c "GRANT {{ item.1 }} TO {{ item.0.name }};" + environment: + PGPASSWORD: "{{ pg_superuser_password }}" + loop: "{{ postgresql_users | subelements('roles', skip_missing=True) }}" + changed_when: false + no_log: true + +# === Create databases === -# Create databases - name: Check if postgresql databases exist ansible.builtin.command: > - {{ postgresql_bin_dir }}/psql -tAc + {{ postgresql_bin_dir }}/psql -h localhost -tAc "SELECT 1 FROM pg_database WHERE datname = '{{ item.name }}';" + environment: + PGPASSWORD: "{{ pg_superuser_password }}" loop: "{{ postgresql_databases }}" register: db_check changed_when: false failed_when: false + no_log: true - name: Create postgresql databases ansible.builtin.command: > - {{ postgresql_bin_dir }}/createdb + {{ postgresql_bin_dir }}/createdb -h localhost --owner={{ item.item.owner }} {{ item.item.name }} + environment: + PGPASSWORD: "{{ pg_superuser_password }}" loop: "{{ db_check.results }}" when: item.stdout != "1" changed_when: true + no_log: true + +# === Write credential files for local access === + +- name: Write .pgpass file for local authentication + ansible.builtin.copy: + content: | + localhost:{{ postgresql_port }}:*:{{ ansible_user_id }}:{{ pg_superuser_password }} + localhost:{{ postgresql_port }}:*:borgmatic:{{ pg_user_passwords['borgmatic'] }} + dest: ~/.pgpass + mode: '0600' + no_log: true diff --git a/ansible/roles/postgresql/templates/pg_hba.conf.j2 b/ansible/roles/postgresql/templates/pg_hba.conf.j2 index 248392b..992897a 100644 --- a/ansible/roles/postgresql/templates/pg_hba.conf.j2 +++ b/ansible/roles/postgresql/templates/pg_hba.conf.j2 @@ -1,12 +1,15 @@ # {{ ansible_managed }} # PostgreSQL Client Authentication Configuration File +# +# All connections require password authentication (scram-sha-256). +# Passwords are managed via 1Password and fetched by ansible at runtime. # TYPE DATABASE USER ADDRESS METHOD -# Local connections (Unix socket) - trust for initial setup -local all all trust +# Local connections (Unix socket) +local all all scram-sha-256 -# IPv4 local connections (password auth for services) +# IPv4 local connections (services connect via TCP) host all all 127.0.0.1/32 scram-sha-256 # IPv6 local connections