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:
parent
248e118102
commit
b7ccca87f3
6 changed files with 162 additions and 84 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 }}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue