--- # Backs up the phoenixd seed and config from vipy to Lapy, gpg encrypted. # # Only seed.dat and phoenix.conf are backed up, on purpose: # - seed.dat is what actually recovers the funds. phoenixd keeps its channel # state with the ACINQ peer, so a wallet is restored from the seed alone. # - restoring a *stale* channel database to a running Lightning node is # dangerous (it can trigger a force close and a penalty), so we do not keep # copies of phoenix.db around to be tempted by. # - phoenix.conf holds the http api password, which is what LNBits and any # other local consumer authenticate with. # # Because both files are static, phoenixd does not need to be stopped. - name: Configure local backup for phoenixd from remote hosts: control gather_facts: no vars_files: - ../../infra_vars.yml - ./phoenixd_vars.yml vars: remote_data_path: "{{ phoenixd_data_dir }}" gpg_recipient: "{{ hostvars['localhost']['gpg_recipient'] | default('') }}" gpg_key_id: "{{ hostvars['localhost']['gpg_key_id'] | default('') }}" tasks: - name: Debug phoenixd backup vars debug: msg: - "remote_host={{ remote_host }}" - "remote_user={{ remote_user }}" - "remote_data_path='{{ remote_data_path }}'" - "local_backup_dir={{ local_backup_dir }}" - "gpg_recipient={{ gpg_recipient }}" - "gpg_key_id={{ gpg_key_id }}" - name: Ensure local backup directory exists ansible.builtin.file: path: "{{ local_backup_dir }}" state: directory mode: "0700" - name: Ensure ~/.local/bin exists ansible.builtin.file: path: "{{ lookup('env', 'HOME') }}/.local/bin" state: directory mode: "0755" - name: Create phoenixd backup script ansible.builtin.copy: dest: "{{ backup_script_path }}" mode: "0750" content: | #!/bin/bash set -euo pipefail if [ -z "{{ gpg_recipient }}" ]; then echo "GPG recipient is not configured. Aborting." exit 1 fi TIMESTAMP=$(date +'%Y-%m-%d') ENCRYPTED_BACKUP="{{ local_backup_dir }}/phoenixd-backup-$TIMESTAMP.tar.gz.gpg" {% if remote_key_file %} SSH_CMD="ssh -i {{ remote_key_file }} -p {{ remote_port }}" {% else %} SSH_CMD="ssh -p {{ remote_port }}" {% endif %} # seed.dat + phoenix.conf only, see the header of the playbook. echo "Creating encrypted backup archive..." $SSH_CMD {{ remote_user }}@{{ remote_host }} \ "sudo tar -czf - -C {{ remote_data_path }} seed.dat phoenix.conf" | \ gpg --batch --yes --encrypt --recipient "{{ gpg_recipient }}" --output "$ENCRYPTED_BACKUP" chmod 600 "$ENCRYPTED_BACKUP" # Rotate old backups (keep 14 days) CUTOFF_DATE=$(date -d '14 days ago' +'%Y-%m-%d') for backup_file in "{{ local_backup_dir }}"/phoenixd-backup-*.tar.gz.gpg; do if [ -f "$backup_file" ]; then # Extract date from filename: phoenixd-backup-YYYY-MM-DD.tar.gz.gpg file_date=$(basename "$backup_file" | sed -n 's/phoenixd-backup-\([0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}\)\.tar\.gz\.gpg/\1/p') if [ -n "$file_date" ] && [ "$file_date" != "$TIMESTAMP" ] && [ "$file_date" \< "$CUTOFF_DATE" ]; then rm -f "$backup_file" fi fi done echo "Backup completed successfully" - name: Ensure cronjob for phoenixd backup exists ansible.builtin.cron: name: "phoenixd backup" user: "{{ lookup('env', 'USER') }}" job: "{{ backup_script_path }}" minute: 15 hour: "9" - name: Run phoenixd backup script to create initial backup ansible.builtin.command: "{{ backup_script_path }}" - name: Verify backup was created block: - name: Get today's date command: date +'%Y-%m-%d' register: today_date changed_when: false - name: Check if backup file exists stat: path: "{{ local_backup_dir }}/phoenixd-backup-{{ today_date.stdout }}.tar.gz.gpg" register: backup_file_stat - name: Verify backup file exists assert: that: - backup_file_stat.stat.exists - backup_file_stat.stat.isreg fail_msg: "Backup file {{ local_backup_dir }}/phoenixd-backup-{{ today_date.stdout }}.tar.gz.gpg was not created" success_msg: "Backup file {{ local_backup_dir }}/phoenixd-backup-{{ today_date.stdout }}.tar.gz.gpg exists" - name: Verify backup file is not empty assert: that: - backup_file_stat.stat.size > 0 fail_msg: "Backup file {{ local_backup_dir }}/phoenixd-backup-{{ today_date.stdout }}.tar.gz.gpg exists but is empty" success_msg: "Backup file size is {{ backup_file_stat.stat.size }} bytes" - name: Remind about the offline seed copy debug: msg: | These encrypted backups are only as safe as your GPG key. Also write the 12 words down offline once: ssh {{ remote_user }}@{{ remote_host }} "sudo cat {{ remote_data_path }}/seed.dat"