1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# The imported shared plays target {{ ansible_limit }} and match nothing
# without it, so a bare run would apply this host's plays and skip theirs.
- name: Require --limit
  hosts: legacy-of-lunatic-kingdom
  gather_facts: false
  tags: always
  tasks:

    - name: Fail without --limit
      ansible.builtin.assert:
        that: ansible_limit is defined
        fail_msg: >-
          Run with --limit legacy-of-lunatic-kingdom; without it the imported
          shared plays match nothing and this playbook half-applies.
        quiet: true

- name: Basic requirements
  hosts: legacy-of-lunatic-kingdom
  become: true
  tasks:

    - name: Install basic packages
      ansible.builtin.apt:
        pkg:
          - git
          - net-tools # ifconfig
          - neovim
          - prometheus-node-exporter
          - prometheus-node-exporter-collectors # apt_info + reboot-required metrics
          - python3-lxml # syncthing config (community.general.xml)
          - rsync
        state: present
        update_cache: true

    - name: Configure node-exporter collectors
      vars:
        # The systemd + apt collectors expose unattended-upgrades health to the
        # node-exporter target (last run, pending security updates, reboot
        # needed). See playbooks/unattended-upgrades.yml.
        node_exporter_args:
          - --collector.textfile.directory=/var/lib/prometheus/node-exporter
          - --collector.systemd
          - --collector.systemd.unit-include=apt-daily.*\.(timer|service)
          - >-
            --collector.filesystem.mount-points-exclude=^/(dev|proc|run|sys|media|var/lib/containers/storage/.+)($|/)
      ansible.builtin.lineinfile:
        path: /etc/default/prometheus-node-exporter
        regexp: '^ARGS='
        line: 'ARGS="{{ node_exporter_args | join(" ") }}"'
        create: true
        mode: '0644'
      notify: Restart node exporter

    # Periodically regenerates apt.prom (pending upgrades, reboot-required) in
    # the textfile directory above.
    - name: Enable node-exporter apt collector
      ansible.builtin.systemd:
        name: prometheus-node-exporter-apt.timer
        enabled: true
        state: started

  handlers:
    - name: Restart node exporter
      ansible.builtin.systemd:
        name: prometheus-node-exporter
        state: restarted

# TODO disallow network access from non-tailscale interfaces
- import_playbook: ../playbooks/secure.yml              # noqa: name[play]
- import_playbook: ../playbooks/tailscale.yml           # noqa: name[play]
- import_playbook: ../playbooks/podman.yml              # noqa: name[play]
- import_playbook: caddy.yml                            # noqa: name[play]
- import_playbook: ../playbooks/syncthing.yml           # noqa: name[play]
- import_playbook: ../playbooks/unattended-upgrades.yml # noqa: name[play]
- import_playbook: ../playbooks/journal-upload.yml      # noqa: name[play]

- name: Host a Restic repository
  tags: restic
  hosts: legacy-of-lunatic-kingdom
  become: true
  tasks:

    # Applied before the account changes below so sftp never briefly depends
    # on the user's shell; internal-sftp serves absolute paths, so the
    # repository URL stays as it is.
    - name: Restrict the restic user to sftp
      ansible.builtin.blockinfile:
        path: /etc/ssh/sshd_config
        block: |
          Match User restic
              ForceCommand internal-sftp
              AllowTcpForwarding no
              X11Forwarding no
        validate: /usr/sbin/sshd -t -f %s
        mode: "0644"
      notify: Restart sshd

    - name: Create Restic user
      ansible.builtin.user:
        name: restic
        system: true
        # sshd's account check under UsePAM no refuses locked ('!') users,
        # which is how Debian creates system users. '*' is not locked but
        # can never match a password.
        password: "*"
        # internal-sftp needs no shell.
        shell: /usr/sbin/nologin
        state: present

    - name: Add SSH key for restic user
      ansible.posix.authorized_key:
        user: restic
        key: "{{ item }}"
        # ForceCommand confines; restrict also kills forwarding and pty at
        # the auth layer.
        key_options: restrict
      loop:
        - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILrlVay1GlpCuhYSUup/K/gRYmltycViPBAdZ8t9OKtu # lotus-land-story
        - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPgX0tA28NO3djYaV++JxggR0t47xou+R0HQVwVjzj8s # 1password

    - name: Create Restic directory
      ansible.builtin.file:
        path: "{{ (data_dir, 'restic') | path_join }}"
        state: directory
        owner: restic
        group: restic
        mode: "0755"

  handlers:
    - name: Restart sshd
      ansible.builtin.systemd:
        name: sshd
        state: restarted

- name: Keep dhcpcd out of resolv.conf
  # Trixie's ifupdown uses dhcpcd for the dhcp method, and it rewrites
  # /etc/resolv.conf on every lease event, stomping the file tailscaled owns
  # until its next check restores MagicDNS. Takes effect at the next dhcpcd
  # start, i.e. a reboot or a restart of the networking service.
  tags: dns
  hosts: legacy-of-lunatic-kingdom
  become: true
  tasks:

    - name: Disable dhcpcd resolv.conf hook
      ansible.builtin.lineinfile:
        path: /etc/dhcpcd.conf
        line: nohook resolv.conf
        state: present

# TODO
# - jellyfin
# - immich

# vim: ft=yaml.ansible