mirror of
https://github.com/fscotto/infra.git
synced 2026-10-03 13:29:58 +00:00
Prepare isolated rootless Gitea Quadlet on Atlas
This commit is contained in:
12
AGENTS.md
12
AGENTS.md
@@ -57,6 +57,8 @@ Ansible-driven personal infrastructure repo for Fedora and Void desktops, Fedora
|
||||
- Server compose render: `podman-compose -f /opt/docker/server/docker-compose.yml config` and `systemctl status podman-compose-server`
|
||||
- Atlas media stack:
|
||||
`ansible-playbook ansible/site.yml --limit atlas --tags storage,sharing,containers --check --diff`
|
||||
- Atlas rootless Gitea staging (does not start Gitea):
|
||||
`ansible-playbook ansible/site.yml --limit atlas --tags gitea --check --diff`
|
||||
- Atlas network/share hardening:
|
||||
`ansible-playbook ansible/site.yml --limit atlas --tags hardening,sharing --check --diff`
|
||||
- Atlas ZFS snapshot retention and scrub timers:
|
||||
@@ -263,9 +265,13 @@ successfully. The first monthly scrub remains a runtime check.
|
||||
- [x] Design the staged Prometheus-to-Atlas Gitea migration in `docs/atlas-gitea-migration.md`.
|
||||
The approved topology keeps NPM on Prometheus and moves HTTPS and public SSH (TCP/2222) together;
|
||||
Gitea must run as a dedicated rootless user Quadlet on Atlas. The rootful-to-rootless data-layout
|
||||
conversion requires an isolated restore test. No data has been moved or traffic changed.
|
||||
- [ ] Prepare a separate Atlas Gitea dataset, disabled rootless user Quadlet, and isolated restore test from the
|
||||
verified Prometheus backup; validate SQLite, repositories, SSH host keys, and target backups.
|
||||
conversion requires an isolated restore test. No Gitea data has been moved or traffic changed.
|
||||
- [x] Prepare the dedicated Atlas Gitea dataset, non-login UID/GID 1101 with a separate rootless Podman
|
||||
sub-ID range, and disabled user Quadlet. On 2026-10-01 the targeted Ansible run and a second idempotent
|
||||
run passed; the generated unit was inactive, with no staging HTTP/SSH listener. POSIX ACLs on only the
|
||||
service-namespace parents grant this account traversal without access to sibling datasets.
|
||||
- [ ] Perform an isolated rootless restore test from the verified Prometheus backup; validate SQLite,
|
||||
repositories, SSH host keys, and target backups before any traffic cutover.
|
||||
- [ ] After an explicit outage approval, perform the final consistent copy and HTTPS/SSH cutover,
|
||||
then remove Gitea from Prometheus' desired stack and backup export without deleting source data.
|
||||
- [ ] Design and deploy Nextcloud as another explicitly temporary Atlas service before Uranus. Give it
|
||||
|
||||
@@ -322,9 +322,11 @@ alla LAN. Dopo la verifica dei servizi, configurare manualmente i Proxy Host NPM
|
||||
negli `AllowedIPs`; aggiungere la VIP Uranus quando esisterà. Dopo il reload di firewalld, Ansible
|
||||
ricarica le reti Podman rootful di Prometheus per conservare DNS e connettività del proxy.
|
||||
|
||||
La migrazione Gitea da Prometheus ad Atlas è pianificata, ma non ancora eseguita, in
|
||||
[`docs/atlas-gitea-migration.md`](docs/atlas-gitea-migration.md). NPM resta su Prometheus;
|
||||
stack sorgente e instradamento pubblico rimangono invariati fino a un cutover separato e validato.
|
||||
La migrazione Gitea da Prometheus ad Atlas è predisposta in
|
||||
[`docs/atlas-gitea-migration.md`](docs/atlas-gitea-migration.md). Atlas ha un dataset e un account
|
||||
dedicati con Quadlet utente rootless inattivo. I dati Gitea non sono ancora stati ripristinati o spostati.
|
||||
NPM resta su Prometheus; stack sorgente e instradamento pubblico rimangono invariati fino a un cutover
|
||||
HTTPS e SSH separato e validato.
|
||||
|
||||
Validare il gateway con:
|
||||
|
||||
|
||||
@@ -298,9 +298,11 @@ and Aegis (`10.0.0.2`). Their state is initialized ex novo in `/zpool/services/d
|
||||
`/zpool/services/data/syncthing`; no source application state is migrated. The music library at
|
||||
`/zpool/media/music` is populated separately.
|
||||
|
||||
The staged, not-yet-executed Gitea move from Prometheus to Atlas is described in
|
||||
[`docs/atlas-gitea-migration.md`](docs/atlas-gitea-migration.md). NPM remains on Prometheus;
|
||||
the source stack and public routes stay unchanged until a separately validated cutover.
|
||||
The Gitea move from Prometheus to Atlas is staged in
|
||||
[`docs/atlas-gitea-migration.md`](docs/atlas-gitea-migration.md). Atlas has a dedicated dataset and
|
||||
non-login account with an inactive rootless user Quadlet. No Gitea data has been restored or moved yet.
|
||||
NPM remains on Prometheus; the source stack and public routes stay unchanged until a separately
|
||||
validated HTTPS and SSH cutover.
|
||||
|
||||
The separate `wireguard_overlay` role manages `wg0` between Prometheus (`10.0.0.1`) and Aegis
|
||||
(`10.0.0.2`), generating private keys once on their respective hosts and exchanging only public keys
|
||||
|
||||
@@ -49,6 +49,11 @@ atlas_zfs_backup_reservation: 500G
|
||||
atlas_zfs_dataset_photobook: media/photobook
|
||||
atlas_mount_root: /zpool
|
||||
atlas_manage_storage: true
|
||||
# Prepare only the isolated rootless Gitea target; no restore, start, or cutover.
|
||||
atlas_manage_gitea: true
|
||||
# Dedicated rootless Podman range; admin owns 100000-165535 on this host.
|
||||
atlas_gitea_subid_start: 165536
|
||||
atlas_gitea_subid_count: 65536
|
||||
atlas_prometheus_pull_start_timer: true
|
||||
atlas_manage_zfs_snapshots: true
|
||||
atlas_zfs_snapshot_prefix: atlas-auto
|
||||
|
||||
@@ -165,6 +165,23 @@ atlas_prometheus_pull_keep_monthly: 12
|
||||
atlas_prometheus_pull_max_age_hours: 24
|
||||
atlas_photobook_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_photobook }}"
|
||||
|
||||
# Staged rootless Gitea target. Preparation never starts the user Quadlet or opens ingress.
|
||||
atlas_manage_gitea: false
|
||||
atlas_gitea_username: gitea
|
||||
atlas_gitea_group: gitea
|
||||
atlas_gitea_uid: 1101
|
||||
atlas_gitea_gid: 1101
|
||||
atlas_gitea_subid_start: 165536
|
||||
atlas_gitea_subid_count: 65536
|
||||
atlas_gitea_home: /var/lib/atlas-gitea
|
||||
atlas_gitea_dataset: "{{ atlas_zfs_pool }}/services/data/gitea"
|
||||
atlas_gitea_mountpoint: "{{ atlas_app_data_mountpoint }}/gitea"
|
||||
atlas_gitea_quadlet_dir: "{{ atlas_gitea_home }}/.config/containers/systemd"
|
||||
atlas_gitea_image: docker.gitea.com/gitea:1.25.2-rootless
|
||||
atlas_gitea_staging_bind_address: 127.0.0.1
|
||||
atlas_gitea_staging_http_port: 3001
|
||||
atlas_gitea_staging_ssh_port: 2223
|
||||
|
||||
atlas_45drives_repo_url: https://repo.45drives.com/repofiles/rocky/45drives-enterprise.repo
|
||||
atlas_45drives_repo_file: /etc/yum.repos.d/45drives-enterprise.repo
|
||||
atlas_45drives_packages:
|
||||
|
||||
133
ansible/roles/profile_atlas/tasks/gitea.yml
Normal file
133
ansible/roles/profile_atlas/tasks/gitea.yml
Normal file
@@ -0,0 +1,133 @@
|
||||
---
|
||||
- name: Prepare the isolated rootless Atlas Gitea target
|
||||
tags: [atlas, gitea]
|
||||
when: atlas_manage_gitea | bool
|
||||
block:
|
||||
- name: Require the existing Atlas application-data dataset
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- atlas_manage_storage | bool
|
||||
- atlas_gitea_dataset == atlas_zfs_pool ~ '/services/data/gitea'
|
||||
- atlas_gitea_mountpoint == atlas_app_data_mountpoint ~ '/gitea'
|
||||
- atlas_gitea_uid | int != atlas_admin_uid | int
|
||||
- atlas_gitea_uid | int != atlas_immich_uid | int
|
||||
- atlas_gitea_gid | int != atlas_admin_gid | int
|
||||
- atlas_gitea_gid | int != atlas_immich_gid | int
|
||||
- atlas_gitea_staging_bind_address == '127.0.0.1'
|
||||
fail_msg: >-
|
||||
Rootless Gitea preparation requires Atlas storage, an isolated service
|
||||
identity and dataset, and loopback-only staging ports.
|
||||
|
||||
- name: Create the dedicated Gitea group
|
||||
ansible.builtin.group:
|
||||
name: "{{ atlas_gitea_group }}"
|
||||
gid: "{{ atlas_gitea_gid }}"
|
||||
system: true
|
||||
state: present
|
||||
|
||||
- name: Create the non-login Gitea service account
|
||||
ansible.builtin.user:
|
||||
name: "{{ atlas_gitea_username }}"
|
||||
uid: "{{ atlas_gitea_uid }}"
|
||||
group: "{{ atlas_gitea_group }}"
|
||||
home: "{{ atlas_gitea_home }}"
|
||||
shell: /sbin/nologin
|
||||
create_home: true
|
||||
system: true
|
||||
state: present
|
||||
|
||||
- name: Restrict the Gitea service home
|
||||
ansible.builtin.file:
|
||||
path: "{{ atlas_gitea_home }}"
|
||||
state: directory
|
||||
owner: "{{ atlas_gitea_username }}"
|
||||
group: "{{ atlas_gitea_group }}"
|
||||
mode: "0700"
|
||||
|
||||
- name: Reserve dedicated rootless UID and GID ranges for Gitea
|
||||
ansible.builtin.lineinfile:
|
||||
path: "{{ item }}"
|
||||
regexp: '^{{ atlas_gitea_username }}:'
|
||||
line: >-
|
||||
{{ atlas_gitea_username }}:{{ atlas_gitea_subid_start }}:{{ atlas_gitea_subid_count }}
|
||||
create: false
|
||||
mode: "0644"
|
||||
loop:
|
||||
- /etc/subuid
|
||||
- /etc/subgid
|
||||
|
||||
- name: Enable POSIX ACLs only on the service-namespace parents
|
||||
community.general.zfs:
|
||||
name: "{{ item }}"
|
||||
state: present
|
||||
extra_zfs_properties:
|
||||
acltype: posix
|
||||
loop:
|
||||
- "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_services }}"
|
||||
- "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_app_data }}"
|
||||
|
||||
- name: Permit Gitea to traverse only the application-data parents
|
||||
ansible.posix.acl:
|
||||
path: "{{ item }}"
|
||||
entity: "{{ atlas_gitea_username }}"
|
||||
etype: user
|
||||
permissions: x
|
||||
state: present
|
||||
loop:
|
||||
- "{{ atlas_services_mountpoint }}"
|
||||
- "{{ atlas_app_data_mountpoint }}"
|
||||
|
||||
- name: Create the dedicated Gitea ZFS dataset
|
||||
community.general.zfs:
|
||||
name: "{{ atlas_gitea_dataset }}"
|
||||
state: present
|
||||
extra_zfs_properties:
|
||||
compression: zstd
|
||||
mountpoint: "{{ atlas_gitea_mountpoint }}"
|
||||
|
||||
- name: Restrict the Gitea dataset and create rootless volume paths
|
||||
ansible.builtin.file:
|
||||
path: "{{ item }}"
|
||||
state: directory
|
||||
owner: "{{ atlas_gitea_username }}"
|
||||
group: "{{ atlas_gitea_group }}"
|
||||
mode: "0700"
|
||||
loop:
|
||||
- "{{ atlas_gitea_mountpoint }}"
|
||||
- "{{ atlas_gitea_mountpoint }}/data"
|
||||
- "{{ atlas_gitea_mountpoint }}/config"
|
||||
- "{{ atlas_gitea_home }}/.config"
|
||||
- "{{ atlas_gitea_home }}/.config/containers"
|
||||
- "{{ atlas_gitea_quadlet_dir }}"
|
||||
|
||||
- name: Enable lingering for the dedicated rootless account
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- loginctl
|
||||
- enable-linger
|
||||
- "{{ atlas_gitea_username }}"
|
||||
creates: "/var/lib/systemd/linger/{{ atlas_gitea_username }}"
|
||||
|
||||
- name: Start the dedicated rootless user manager
|
||||
ansible.builtin.systemd:
|
||||
name: "user@{{ atlas_gitea_uid }}.service"
|
||||
state: started
|
||||
when: not ansible_check_mode
|
||||
|
||||
- name: Render the disabled rootless Gitea Quadlet
|
||||
ansible.builtin.template:
|
||||
src: atlas-gitea.container.j2
|
||||
dest: "{{ atlas_gitea_quadlet_dir }}/atlas-gitea.container"
|
||||
owner: "{{ atlas_gitea_username }}"
|
||||
group: "{{ atlas_gitea_group }}"
|
||||
mode: "0644"
|
||||
|
||||
- name: Reload the rootless Gitea user manager without starting Gitea
|
||||
become_user: "{{ atlas_gitea_username }}"
|
||||
ansible.builtin.systemd:
|
||||
scope: user
|
||||
daemon_reload: true
|
||||
environment:
|
||||
XDG_RUNTIME_DIR: "/run/user/{{ atlas_gitea_uid }}"
|
||||
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ atlas_gitea_uid }}/bus"
|
||||
when: not ansible_check_mode
|
||||
@@ -14,6 +14,9 @@
|
||||
- name: Import Atlas storage tasks
|
||||
ansible.builtin.import_tasks: storage.yml
|
||||
|
||||
- name: Import staged Atlas rootless Gitea tasks
|
||||
ansible.builtin.import_tasks: gitea.yml
|
||||
|
||||
- name: Import Atlas ZFS maintenance tasks
|
||||
ansible.builtin.import_tasks: zfs_maintenance.yml
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
# Managed by Ansible. Staging only: no [Install], no automatic start.
|
||||
[Unit]
|
||||
Description=Atlas rootless Gitea staging target
|
||||
RequiresMountsFor={{ atlas_gitea_mountpoint }}
|
||||
|
||||
[Container]
|
||||
ContainerName=atlas-gitea
|
||||
Image={{ atlas_gitea_image }}
|
||||
UserNS=keep-id:uid=1000,gid=1000
|
||||
PublishPort={{ atlas_gitea_staging_bind_address }}:{{ atlas_gitea_staging_http_port }}:3000
|
||||
PublishPort={{ atlas_gitea_staging_bind_address }}:{{ atlas_gitea_staging_ssh_port }}:2222
|
||||
Volume={{ atlas_gitea_mountpoint }}/data:/var/lib/gitea:Z
|
||||
Volume={{ atlas_gitea_mountpoint }}/config:/etc/gitea:Z
|
||||
NoNewPrivileges=true
|
||||
DropCapability=all
|
||||
|
||||
[Service]
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
TimeoutStartSec=900
|
||||
@@ -35,6 +35,15 @@ Uranus; NPM remains on Prometheus.
|
||||
|
||||
## Phase 1: prepare without traffic changes
|
||||
|
||||
Preparation completed on 2026-10-01: Ansible created
|
||||
`zpool/services/data/gitea`, a dedicated non-login `gitea` account (UID/GID
|
||||
1101), separate subordinate IDs, parent-dataset traverse ACLs, and an inactive
|
||||
user Quadlet under `/var/lib/atlas-gitea/.config/containers/systemd/`. The
|
||||
Quadlet has no `[Install]` section and, until the final cutover, binds only
|
||||
loopback staging ports 3001/2223 if started manually. A second targeted
|
||||
Ansible run changed nothing; the generated service was inactive and neither
|
||||
staging port listened. **No Gitea payload has been restored to the target.**
|
||||
|
||||
1. Provision a dedicated target dataset and non-login service identity via
|
||||
Ansible, keeping UID/GID distinct from Atlas' reserved Immich `1100`.
|
||||
Install the user Quadlet in that identity's
|
||||
|
||||
Reference in New Issue
Block a user