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 <noreply@anthropic.com>
This commit is contained in:
Erich Blume 2026-01-16 08:06:29 -08:00
commit b7ccca87f3
6 changed files with 162 additions and 84 deletions

View file

@ -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

View file

@ -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

View file

@ -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 }}

View file

@ -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

View file

@ -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

View file

@ -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