mirror of
https://github.com/fscotto/infra.git
synced 2026-10-03 21:39:50 +00:00
Prepare gated Gitea HTTPS and SSH cutover
This commit is contained in:
12
AGENTS.md
12
AGENTS.md
@@ -65,6 +65,9 @@ Ansible-driven personal infrastructure repo for Fedora and Void desktops, Fedora
|
|||||||
`ansible-playbook ansible/site.yml --limit atlas --tags gitea_final_restore --check --diff -e atlas_gitea_final_restore=true`
|
`ansible-playbook ansible/site.yml --limit atlas --tags gitea_final_restore --check --diff -e atlas_gitea_final_restore=true`
|
||||||
- Prometheus final Gitea export helper (dry-run installs only; outage action remains opt-in):
|
- Prometheus final Gitea export helper (dry-run installs only; outage action remains opt-in):
|
||||||
`ansible-playbook ansible/site.yml --limit prometheus --tags gitea_final_export --check --diff`
|
`ansible-playbook ansible/site.yml --limit prometheus --tags gitea_final_export --check --diff`
|
||||||
|
- Gitea cutover network configuration before activation:
|
||||||
|
`ansible-playbook ansible/site.yml --limit prometheus --tags gitea_cutover,prometheus_backup --check --diff -e server_gitea_on_atlas=true`
|
||||||
|
and `ansible-playbook ansible/site.yml --limit atlas --tags gitea --check --diff`
|
||||||
- Atlas network/share hardening:
|
- Atlas network/share hardening:
|
||||||
`ansible-playbook ansible/site.yml --limit atlas --tags hardening,sharing --check --diff`
|
`ansible-playbook ansible/site.yml --limit atlas --tags hardening,sharing --check --diff`
|
||||||
- Atlas ZFS snapshot retention and scrub timers:
|
- Atlas ZFS snapshot retention and scrub timers:
|
||||||
@@ -289,7 +292,8 @@ successfully. The first monthly scrub remains a runtime check.
|
|||||||
and snapshot mounts were removed, the Borg service ended successfully, and the pool was healthy.
|
and snapshot mounts were removed, the Borg service ended successfully, and the pool was healthy.
|
||||||
- [ ] Include the new Gitea dataset in the next UUID-bound offline USB version and test a file restore
|
- [ ] Include the new Gitea dataset in the next UUID-bound offline USB version and test a file restore
|
||||||
from that version before accepting production writes; the UUID-bound disk is connected but its
|
from that version before accepting production writes; the UUID-bound disk is connected but its
|
||||||
LUKS mapper is closed, so the manual backup still requires interactive unlock.
|
LUKS mapper is closed, so the manual backup still requires interactive unlock. On 2026-10-01 the
|
||||||
|
operator could not unlock it and chose to defer traffic cutover until this check passes.
|
||||||
- [x] Install a separate opt-in final Gitea export helper on Prometheus. Its 2026-10-01 targeted
|
- [x] Install a separate opt-in final Gitea export helper on Prometheus. Its 2026-10-01 targeted
|
||||||
deployment and `bash -n` passed while Gitea and NPM stayed running. It refuses an active export
|
deployment and `bash -n` passed while Gitea and NPM stayed running. It refuses an active export
|
||||||
timer, stops only Gitea, verifies SQLite, publishes a checksum-verified Gitea-only version for
|
timer, stops only Gitea, verifies SQLite, publishes a checksum-verified Gitea-only version for
|
||||||
@@ -299,6 +303,12 @@ successfully. The first monthly scrub remains a runtime check.
|
|||||||
layout before replacing the marked rehearsal, and rolls back a failed swap. Synthetic success
|
layout before replacing the marked rehearsal, and rolls back a failed swap. Synthetic success
|
||||||
and rollback tests and a second idempotent rehearsal run passed on 2026-10-01; the final gate
|
and rollback tests and a second idempotent rehearsal run passed on 2026-10-01; the final gate
|
||||||
has **not** been invoked.
|
has **not** been invoked.
|
||||||
|
- [x] Prepare, but do not activate, the Atlas LAN rootless Quadlet and Prometheus TCP/2222 socket
|
||||||
|
proxy. NPM's two existing `gitea:3000` Proxy Hosts will resolve that name to Atlas through a
|
||||||
|
managed Compose `extra_hosts` entry after the source container is removed; no NPM database edit
|
||||||
|
is needed. The future-mode Prometheus check-run passed, both current-mode runs were idempotent,
|
||||||
|
the new systemd units passed verification, and source HTTP remained 200 on 2026-10-01. Public
|
||||||
|
2222 is closed and the target remains inactive until the explicit cutover flags are enabled.
|
||||||
- [ ] After an explicit outage approval, perform the final consistent copy and HTTPS/SSH 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.
|
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
|
- [ ] Design and deploy Nextcloud as another explicitly temporary Atlas service before Uranus. Give it
|
||||||
|
|||||||
@@ -90,23 +90,22 @@ server_backup_export_start_timer: false
|
|||||||
# Explicit Gitea cutover helper: installed separately from any outage action.
|
# Explicit Gitea cutover helper: installed separately from any outage action.
|
||||||
server_gitea_cutover_tools_enabled: false
|
server_gitea_cutover_tools_enabled: false
|
||||||
server_gitea_final_export: false
|
server_gitea_final_export: false
|
||||||
|
server_gitea_on_atlas: false
|
||||||
|
server_gitea_atlas_address: "{{ hostvars['atlas'].ansible_host }}"
|
||||||
|
server_gitea_ssh_public_port: 2222
|
||||||
|
server_gitea_ssh_target_port: 2222
|
||||||
server_backup_export_source_keep: 3
|
server_backup_export_source_keep: 3
|
||||||
server_backup_export_paths:
|
server_backup_export_paths: >-
|
||||||
- opt/npm/data
|
{{ ['opt/npm/data', 'opt/npm/letsencrypt']
|
||||||
- opt/npm/letsencrypt
|
+ ([] if server_gitea_on_atlas | bool else ['opt/gitea/data', 'home/git/.ssh'])
|
||||||
- opt/gitea/data
|
+ ['opt/docker/server/docker-compose.yml',
|
||||||
- home/git/.ssh
|
'etc/systemd/system/podman-compose-server.service',
|
||||||
- opt/docker/server/docker-compose.yml
|
'etc/ssh/sshd_config', 'etc/ssh/sshd_config.d',
|
||||||
- etc/systemd/system/podman-compose-server.service
|
'etc/firewalld', 'etc/wireguard/wg0.conf'] }}
|
||||||
- etc/ssh/sshd_config
|
server_backup_export_excludes: >-
|
||||||
- etc/ssh/sshd_config.d
|
{{ ['opt/npm/data/logs']
|
||||||
- etc/firewalld
|
+ ([] if server_gitea_on_atlas | bool else
|
||||||
- etc/wireguard/wg0.conf
|
['opt/gitea/data/gitea/log', 'opt/gitea/data/gitea/tmp',
|
||||||
server_backup_export_excludes:
|
'opt/gitea/data/gitea/sessions', 'opt/gitea/data/gitea/indexers']) }}
|
||||||
- opt/npm/data/logs
|
|
||||||
- opt/gitea/data/gitea/log
|
|
||||||
- opt/gitea/data/gitea/tmp
|
|
||||||
- opt/gitea/data/gitea/sessions
|
|
||||||
- opt/gitea/data/gitea/indexers
|
|
||||||
server_ssh_authorized_keys: []
|
server_ssh_authorized_keys: []
|
||||||
server_ssh_authorized_key_directory: "{{ server_user_home }}/.ssh/authorized_keys.d"
|
server_ssh_authorized_key_directory: "{{ server_user_home }}/.ssh/authorized_keys.d"
|
||||||
|
|||||||
@@ -178,6 +178,10 @@ atlas_gitea_dataset: "{{ atlas_zfs_pool }}/services/data/gitea"
|
|||||||
atlas_gitea_mountpoint: "{{ atlas_app_data_mountpoint }}/gitea"
|
atlas_gitea_mountpoint: "{{ atlas_app_data_mountpoint }}/gitea"
|
||||||
atlas_gitea_quadlet_dir: "{{ atlas_gitea_home }}/.config/containers/systemd"
|
atlas_gitea_quadlet_dir: "{{ atlas_gitea_home }}/.config/containers/systemd"
|
||||||
atlas_gitea_image: docker.gitea.com/gitea:1.25.2-rootless
|
atlas_gitea_image: docker.gitea.com/gitea:1.25.2-rootless
|
||||||
|
atlas_gitea_production_enabled: false
|
||||||
|
atlas_gitea_bind_address: "{{ ansible_host }}"
|
||||||
|
atlas_gitea_http_port: 3000
|
||||||
|
atlas_gitea_ssh_port: 2222
|
||||||
atlas_gitea_staging_bind_address: 127.0.0.1
|
atlas_gitea_staging_bind_address: 127.0.0.1
|
||||||
atlas_gitea_staging_http_port: 3001
|
atlas_gitea_staging_http_port: 3001
|
||||||
atlas_gitea_staging_ssh_port: 2223
|
atlas_gitea_staging_ssh_port: 2223
|
||||||
|
|||||||
@@ -14,10 +14,25 @@
|
|||||||
- atlas_gitea_gid | int != atlas_admin_gid | int
|
- atlas_gitea_gid | int != atlas_admin_gid | int
|
||||||
- atlas_gitea_gid | int != atlas_immich_gid | int
|
- atlas_gitea_gid | int != atlas_immich_gid | int
|
||||||
- atlas_gitea_staging_bind_address == '127.0.0.1'
|
- atlas_gitea_staging_bind_address == '127.0.0.1'
|
||||||
|
- not (atlas_gitea_production_enabled | bool) or atlas_manage_firewall | bool
|
||||||
|
- not (atlas_gitea_production_enabled | bool) or atlas_gitea_bind_address == ansible_host
|
||||||
fail_msg: >-
|
fail_msg: >-
|
||||||
Rootless Gitea preparation requires Atlas storage, an isolated service
|
Rootless Gitea preparation requires Atlas storage, an isolated service
|
||||||
identity and dataset, and loopback-only staging ports.
|
identity and dataset, and loopback-only staging ports.
|
||||||
|
|
||||||
|
- name: Inspect the final-restore marker before production activation
|
||||||
|
ansible.builtin.stat:
|
||||||
|
path: "{{ atlas_gitea_mountpoint }}/.final-sha256"
|
||||||
|
register: atlas_gitea_final_marker
|
||||||
|
when: atlas_gitea_production_enabled | bool
|
||||||
|
|
||||||
|
- name: Refuse production activation without the final consistent restore
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- atlas_gitea_final_marker.stat.isreg | default(false)
|
||||||
|
fail_msg: Restore the final stopped-source Gitea export before enabling production.
|
||||||
|
when: atlas_gitea_production_enabled | bool
|
||||||
|
|
||||||
- name: Create the dedicated Gitea group
|
- name: Create the dedicated Gitea group
|
||||||
ansible.builtin.group:
|
ansible.builtin.group:
|
||||||
name: "{{ atlas_gitea_group }}"
|
name: "{{ atlas_gitea_group }}"
|
||||||
@@ -114,7 +129,7 @@
|
|||||||
state: started
|
state: started
|
||||||
when: not ansible_check_mode
|
when: not ansible_check_mode
|
||||||
|
|
||||||
- name: Render the disabled rootless Gitea Quadlet
|
- name: Render the rootless Gitea Quadlet
|
||||||
ansible.builtin.template:
|
ansible.builtin.template:
|
||||||
src: atlas-gitea.container.j2
|
src: atlas-gitea.container.j2
|
||||||
dest: "{{ atlas_gitea_quadlet_dir }}/atlas-gitea.container"
|
dest: "{{ atlas_gitea_quadlet_dir }}/atlas-gitea.container"
|
||||||
@@ -122,6 +137,20 @@
|
|||||||
group: "{{ atlas_gitea_group }}"
|
group: "{{ atlas_gitea_group }}"
|
||||||
mode: "0644"
|
mode: "0644"
|
||||||
|
|
||||||
|
- name: Permit only Aegis to reach production Gitea HTTP and SSH
|
||||||
|
ansible.posix.firewalld:
|
||||||
|
rich_rule: >-
|
||||||
|
rule family="ipv4" source address="{{ atlas_aegis_ip }}"
|
||||||
|
port port="{{ item }}" protocol="tcp" accept
|
||||||
|
zone: "{{ atlas_firewalld_zone }}"
|
||||||
|
state: "{{ 'enabled' if atlas_gitea_production_enabled | bool else 'disabled' }}"
|
||||||
|
permanent: true
|
||||||
|
immediate: true
|
||||||
|
loop:
|
||||||
|
- "{{ atlas_gitea_http_port }}"
|
||||||
|
- "{{ atlas_gitea_ssh_port }}"
|
||||||
|
when: atlas_manage_firewall | bool
|
||||||
|
|
||||||
- name: Reload the rootless Gitea user manager without starting Gitea
|
- name: Reload the rootless Gitea user manager without starting Gitea
|
||||||
become_user: "{{ atlas_gitea_username }}"
|
become_user: "{{ atlas_gitea_username }}"
|
||||||
ansible.builtin.systemd:
|
ansible.builtin.systemd:
|
||||||
@@ -131,3 +160,17 @@
|
|||||||
XDG_RUNTIME_DIR: "/run/user/{{ atlas_gitea_uid }}"
|
XDG_RUNTIME_DIR: "/run/user/{{ atlas_gitea_uid }}"
|
||||||
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ atlas_gitea_uid }}/bus"
|
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ atlas_gitea_uid }}/bus"
|
||||||
when: not ansible_check_mode
|
when: not ansible_check_mode
|
||||||
|
|
||||||
|
- name: Start and enable the rootless Gitea user Quadlet after final restore
|
||||||
|
become_user: "{{ atlas_gitea_username }}"
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: atlas-gitea.service
|
||||||
|
scope: user
|
||||||
|
state: started
|
||||||
|
enabled: true
|
||||||
|
environment:
|
||||||
|
XDG_RUNTIME_DIR: "/run/user/{{ atlas_gitea_uid }}"
|
||||||
|
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ atlas_gitea_uid }}/bus"
|
||||||
|
when:
|
||||||
|
- atlas_gitea_production_enabled | bool
|
||||||
|
- not ansible_check_mode
|
||||||
|
|||||||
@@ -1,14 +1,19 @@
|
|||||||
# Managed by Ansible. Staging only: no [Install], no automatic start.
|
# Managed by Ansible. Staging does not start automatically.
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=Atlas rootless Gitea staging target
|
Description=Atlas rootless Gitea
|
||||||
RequiresMountsFor={{ atlas_gitea_mountpoint }}
|
RequiresMountsFor={{ atlas_gitea_mountpoint }}
|
||||||
|
|
||||||
[Container]
|
[Container]
|
||||||
ContainerName=atlas-gitea
|
ContainerName=atlas-gitea
|
||||||
Image={{ atlas_gitea_image }}
|
Image={{ atlas_gitea_image }}
|
||||||
UserNS=keep-id:uid=1000,gid=1000
|
UserNS=keep-id:uid=1000,gid=1000
|
||||||
|
{% if atlas_gitea_production_enabled | bool %}
|
||||||
|
PublishPort={{ atlas_gitea_bind_address }}:{{ atlas_gitea_http_port }}:3000
|
||||||
|
PublishPort={{ atlas_gitea_bind_address }}:{{ atlas_gitea_ssh_port }}:2222
|
||||||
|
{% else %}
|
||||||
PublishPort={{ atlas_gitea_staging_bind_address }}:{{ atlas_gitea_staging_http_port }}:3000
|
PublishPort={{ atlas_gitea_staging_bind_address }}:{{ atlas_gitea_staging_http_port }}:3000
|
||||||
PublishPort={{ atlas_gitea_staging_bind_address }}:{{ atlas_gitea_staging_ssh_port }}:2222
|
PublishPort={{ atlas_gitea_staging_bind_address }}:{{ atlas_gitea_staging_ssh_port }}:2222
|
||||||
|
{% endif %}
|
||||||
Volume={{ atlas_gitea_mountpoint }}/data:/var/lib/gitea:Z
|
Volume={{ atlas_gitea_mountpoint }}/data:/var/lib/gitea:Z
|
||||||
Volume={{ atlas_gitea_mountpoint }}/config:/etc/gitea:Z
|
Volume={{ atlas_gitea_mountpoint }}/config:/etc/gitea:Z
|
||||||
NoNewPrivileges=true
|
NoNewPrivileges=true
|
||||||
@@ -18,3 +23,8 @@ DropCapability=all
|
|||||||
Restart=on-failure
|
Restart=on-failure
|
||||||
RestartSec=10
|
RestartSec=10
|
||||||
TimeoutStartSec=900
|
TimeoutStartSec=900
|
||||||
|
{% if atlas_gitea_production_enabled | bool %}
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
|
{% endif %}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@
|
|||||||
when: server_backup_export_enabled | bool
|
when: server_backup_export_enabled | bool
|
||||||
|
|
||||||
- name: Install Prometheus backup export helper
|
- name: Install Prometheus backup export helper
|
||||||
tags: [services, backup, prometheus_backup]
|
tags: [services, backup, prometheus_backup, gitea_cutover]
|
||||||
ansible.builtin.template:
|
ansible.builtin.template:
|
||||||
src: prometheus-backup-export.sh.j2
|
src: prometheus-backup-export.sh.j2
|
||||||
dest: /usr/local/sbin/prometheus-backup-export
|
dest: /usr/local/sbin/prometheus-backup-export
|
||||||
|
|||||||
61
ansible/roles/profile_server/tasks/gitea_ssh_proxy.yml
Normal file
61
ansible/roles/profile_server/tasks/gitea_ssh_proxy.yml
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
---
|
||||||
|
- name: Validate the Prometheus Gitea SSH cutover inputs
|
||||||
|
tags: [services, gitea_cutover]
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- server_gitea_cutover_tools_enabled | bool
|
||||||
|
- server_gitea_atlas_address is match('^[0-9]{1,3}(\.[0-9]{1,3}){3}$')
|
||||||
|
- server_gitea_ssh_public_port | int > 1024
|
||||||
|
- server_gitea_ssh_public_port | int < 65536
|
||||||
|
- server_gitea_ssh_target_port | int > 1024
|
||||||
|
- server_gitea_ssh_target_port | int < 65536
|
||||||
|
- server_gitea_ssh_public_port | int != 22
|
||||||
|
fail_msg: Keep administrative SSH on 22 and provide the Atlas rootless Gitea SSH endpoint.
|
||||||
|
when: server_gitea_on_atlas | bool
|
||||||
|
|
||||||
|
- name: Install the Gitea SSH socket proxy units without activating them
|
||||||
|
tags: [services, gitea_cutover]
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: "{{ item }}.j2"
|
||||||
|
dest: "/etc/systemd/system/{{ item }}"
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: "0644"
|
||||||
|
loop:
|
||||||
|
- prometheus-gitea-ssh-proxy.socket
|
||||||
|
- prometheus-gitea-ssh-proxy.service
|
||||||
|
loop_control:
|
||||||
|
label: "{{ item }}"
|
||||||
|
register: server_gitea_ssh_proxy_units
|
||||||
|
when: server_gitea_cutover_tools_enabled | bool
|
||||||
|
|
||||||
|
- name: Reload systemd after Gitea SSH proxy unit changes
|
||||||
|
tags: [services, gitea_cutover]
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
daemon_reload: true
|
||||||
|
when:
|
||||||
|
- server_gitea_cutover_tools_enabled | bool
|
||||||
|
- server_gitea_ssh_proxy_units is changed
|
||||||
|
- not ansible_check_mode
|
||||||
|
|
||||||
|
- name: Manage the public Gitea SSH socket separately from administrative SSH
|
||||||
|
tags: [services, gitea_cutover]
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: prometheus-gitea-ssh-proxy.socket
|
||||||
|
state: "{{ 'started' if server_gitea_on_atlas | bool else 'stopped' }}"
|
||||||
|
enabled: "{{ server_gitea_on_atlas | bool }}"
|
||||||
|
when:
|
||||||
|
- server_gitea_cutover_tools_enabled | bool
|
||||||
|
- not ansible_check_mode
|
||||||
|
|
||||||
|
- name: Open only the public Gitea SSH port after cutover
|
||||||
|
tags: [services, gitea_cutover]
|
||||||
|
ansible.posix.firewalld:
|
||||||
|
port: "{{ server_gitea_ssh_public_port }}/tcp"
|
||||||
|
zone: "{{ server_firewalld_zone }}"
|
||||||
|
state: "{{ 'enabled' if server_gitea_on_atlas | bool else 'disabled' }}"
|
||||||
|
permanent: true
|
||||||
|
immediate: true
|
||||||
|
when:
|
||||||
|
- server_gitea_cutover_tools_enabled | bool
|
||||||
|
- server_firewall_backend == 'firewalld'
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
label: "{{ item.dest }}"
|
label: "{{ item.dest }}"
|
||||||
|
|
||||||
- name: Render server templates
|
- name: Render server templates
|
||||||
tags: [dotfiles, dotfiles:server]
|
tags: [dotfiles, dotfiles:server, gitea_cutover]
|
||||||
ansible.builtin.template:
|
ansible.builtin.template:
|
||||||
src: "{{ item.src }}"
|
src: "{{ item.src }}"
|
||||||
dest: "{{ item.dest if item.dest.startswith('/') else server_user_home ~ '/' ~ item.dest }}"
|
dest: "{{ item.dest if item.dest.startswith('/') else server_user_home ~ '/' ~ item.dest }}"
|
||||||
@@ -62,6 +62,9 @@
|
|||||||
- name: Import explicit Prometheus Gitea final-export tasks
|
- name: Import explicit Prometheus Gitea final-export tasks
|
||||||
ansible.builtin.import_tasks: gitea_final_export.yml
|
ansible.builtin.import_tasks: gitea_final_export.yml
|
||||||
|
|
||||||
|
- name: Import Prometheus Gitea SSH proxy tasks
|
||||||
|
ansible.builtin.import_tasks: gitea_ssh_proxy.yml
|
||||||
|
|
||||||
- name: Ensure server SSH authorized key fragments directory exists
|
- name: Ensure server SSH authorized key fragments directory exists
|
||||||
tags: [services, ssh]
|
tags: [services, ssh]
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ stack_stopped=true
|
|||||||
systemctl stop "$stack_unit"
|
systemctl stop "$stack_unit"
|
||||||
tar --acls --xattrs --selinux "${excludes[@]}" -C / -cf "$stage/payload.tar" "${paths[@]}"
|
tar --acls --xattrs --selinux "${excludes[@]}" -C / -cf "$stage/payload.tar" "${paths[@]}"
|
||||||
systemctl start "$stack_unit"
|
systemctl start "$stack_unit"
|
||||||
for container in nginx-proxy-manager gitea; do
|
for container in nginx-proxy-manager{% if not server_gitea_on_atlas | bool %} gitea{% endif %}; do
|
||||||
running=false
|
running=false
|
||||||
for _ in {1..30}; do
|
for _ in {1..30}; do
|
||||||
if [[ $(podman inspect --format '{{ '{{.State.Running}}' }}' "$container" 2>/dev/null) == true ]]; then
|
if [[ $(podman inspect --format '{{ '{{.State.Running}}' }}' "$container" 2>/dev/null) == true ]]; then
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Forward public Gitea SSH to Atlas through Aegis
|
||||||
|
Requires=prometheus-gitea-ssh-proxy.socket
|
||||||
|
After=network-online.target wg-quick@wg0.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
ExecStart=/usr/lib/systemd/systemd-socket-proxyd {{ server_gitea_atlas_address }}:{{ server_gitea_ssh_target_port }}
|
||||||
|
DynamicUser=true
|
||||||
|
NoNewPrivileges=true
|
||||||
|
ProtectSystem=strict
|
||||||
|
ProtectHome=true
|
||||||
|
PrivateTmp=true
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Public Gitea SSH socket on Prometheus
|
||||||
|
|
||||||
|
[Socket]
|
||||||
|
ListenStream=0.0.0.0:{{ server_gitea_ssh_public_port }}
|
||||||
|
NoDelay=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=sockets.target
|
||||||
@@ -13,6 +13,10 @@ services:
|
|||||||
- "127.0.0.1:81:81"
|
- "127.0.0.1:81:81"
|
||||||
extra_hosts:
|
extra_hosts:
|
||||||
- "host.containers.internal:host-gateway"
|
- "host.containers.internal:host-gateway"
|
||||||
|
{% if server_gitea_on_atlas | bool %}
|
||||||
|
# Keep both existing NPM Proxy Hosts unchanged; their gitea name now resolves to Atlas.
|
||||||
|
- "gitea:{{ server_gitea_atlas_address }}"
|
||||||
|
{% endif %}
|
||||||
volumes:
|
volumes:
|
||||||
- "/opt/npm/data:/data{{ ':' ~ selinux_volume_option if selinux_volume_option else '' }}"
|
- "/opt/npm/data:/data{{ ':' ~ selinux_volume_option if selinux_volume_option else '' }}"
|
||||||
- "/opt/npm/letsencrypt:/etc/letsencrypt{{ ':' ~ selinux_volume_option if selinux_volume_option else '' }}"
|
- "/opt/npm/letsencrypt:/etc/letsencrypt{{ ':' ~ selinux_volume_option if selinux_volume_option else '' }}"
|
||||||
@@ -38,6 +42,7 @@ services:
|
|||||||
# networks:
|
# networks:
|
||||||
# - web
|
# - web
|
||||||
|
|
||||||
|
{% if not server_gitea_on_atlas | bool %}
|
||||||
gitea:
|
gitea:
|
||||||
image: docker.gitea.com/gitea:1.25.2
|
image: docker.gitea.com/gitea:1.25.2
|
||||||
container_name: gitea
|
container_name: gitea
|
||||||
@@ -55,6 +60,7 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "3000:3000"
|
- "3000:3000"
|
||||||
- "127.0.0.1:222:22"
|
- "127.0.0.1:222:22"
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ Uranus; NPM remains on Prometheus.
|
|||||||
and pull succeeded. The intended target is a separate
|
and pull succeeded. The intended target is a separate
|
||||||
`/zpool/services/data/gitea` dataset, not `Archive` or the backup dataset.
|
`/zpool/services/data/gitea` dataset, not `Archive` or the backup dataset.
|
||||||
- The approved cutover keeps NPM on Prometheus, changes the two HTTP Proxy
|
- The approved cutover keeps NPM on Prometheus, changes the two HTTP Proxy
|
||||||
Hosts to Atlas over the Prometheus--Aegis gateway, and offers public Gitea
|
Hosts' effective upstream to Atlas over the Prometheus--Aegis gateway, and offers public Gitea
|
||||||
SSH on port 2222 via the same gateway. Prometheus port 22 is unchanged.
|
SSH on port 2222 via the same gateway. Prometheus port 22 is unchanged.
|
||||||
HTTPS and SSH must be validated together before declaring cutover.
|
HTTPS and SSH must be validated together before declaring cutover.
|
||||||
- Run Gitea as a **rootless user Quadlet** under a dedicated, non-login Atlas
|
- Run Gitea as a **rootless user Quadlet** under a dedicated, non-login Atlas
|
||||||
@@ -118,36 +118,58 @@ only metadata marked `gitea-cutover`, validates a private staged replacement,
|
|||||||
and swaps it for the marked rehearsal. The swap and its rollback path passed
|
and swaps it for the marked rehearsal. The swap and its rollback path passed
|
||||||
synthetic tests on 2026-10-01; the gate has not been used on live Gitea data.
|
synthetic tests on 2026-10-01; the gate has not been used on live Gitea data.
|
||||||
|
|
||||||
|
The network change is also prepared but inactive. `server_gitea_on_atlas=true`
|
||||||
|
removes the rootful Gitea service from the desired Prometheus Compose stack,
|
||||||
|
adds `gitea:192.168.178.55` to NPM's container hosts file, and removes Gitea
|
||||||
|
from future Prometheus backup exports. Both existing NPM Proxy Host records
|
||||||
|
remain at `gitea:3000`, but that name then resolves to Atlas; no direct SQLite
|
||||||
|
edit or NPM login is required. A separate systemd socket on public TCP/2222
|
||||||
|
proxies SSH to Atlas TCP/2222 over the gateway, leaving administrative TCP/22
|
||||||
|
unchanged. Atlas' production flag changes the user Quadlet from loopback
|
||||||
|
staging ports to LAN ports 3000/2222, grants only Aegis access in firewalld,
|
||||||
|
and starts it **only** after the `.final-sha256` marker exists. Neither flag
|
||||||
|
is enabled yet. The future Prometheus configuration passed a check-run; the
|
||||||
|
installed socket units passed `systemd-analyze verify` while remaining
|
||||||
|
inactive. Source Gitea still answered HTTP 200 after preparation.
|
||||||
|
|
||||||
|
The operator cannot unlock the UUID-bound USB disk now and chose to defer
|
||||||
|
traffic activation until a new USB version covers Gitea and its file restore
|
||||||
|
passes. This blocks the final export, production flags, and public cutover;
|
||||||
|
the prepared configuration alone does not constitute a migration.
|
||||||
|
|
||||||
1. Agree on an outage and record source/target versions, pool health, the
|
1. Agree on an outage and record source/target versions, pool health, the
|
||||||
latest backups, SSH host-key fingerprints, and both current NPM routes.
|
latest backups, SSH host-key fingerprints, and both current NPM routes.
|
||||||
Stop the Prometheus export timer for the change window so it cannot
|
Stop the Prometheus export timer for the change window so it cannot
|
||||||
restart the old Compose stack unexpectedly.
|
restart the old Compose stack unexpectedly.
|
||||||
2. Quiesce source writes. Run one final consistent Prometheus export, pull it
|
2. Quiesce source writes with the final-export helper: it stops Gitea before
|
||||||
to Atlas, verify checksum and timestamp, then stop the source Gitea. Keep
|
the consistent export and leaves it stopped after success. Pull that export
|
||||||
|
to Atlas and verify checksum and timestamp. Keep
|
||||||
`/opt/gitea/data` and `/home/git/.ssh` intact for rollback. Do not allow
|
`/opt/gitea/data` and `/home/git/.ssh` intact for rollback. Do not allow
|
||||||
source Gitea to restart after accepting writes on Atlas.
|
source Gitea to restart after accepting writes on Atlas.
|
||||||
3. Restore the final Gitea-only payload to the target and repeat integrity
|
3. Restore the final Gitea-only payload to the target and repeat integrity
|
||||||
checks. Set Gitea's advertised SSH port to 2222 while retaining its
|
checks. Verify its advertised SSH port is 2222, its existing HTTPS
|
||||||
existing HTTPS `ROOT_URL` and verified host keys. Start the pinned rootless
|
`ROOT_URL`, repositories, LFS/attachments, and SSH host-key identity. Enable
|
||||||
Atlas user Quadlet,
|
the production Atlas Quadlet only after the final-restore marker exists;
|
||||||
initially without public ingress; validate local HTTP, SQLite, repositories,
|
its firewall permits only Aegis to reach HTTP and SSH. Validate local HTTP
|
||||||
LFS/attachments, and SSH host-key identity.
|
and the target service before switching NPM.
|
||||||
4. Permit only Aegis' source-NAT address to reach Atlas' Gitea HTTP and SSH
|
4. Enable the public TCP/2222 socket proxy on Prometheus to Atlas over Aegis
|
||||||
ports. Enable the public TCP/2222 forward on Prometheus to Atlas over Aegis
|
without changing administrative TCP/22. Switch Prometheus to the desired
|
||||||
without changing administrative TCP/22. Update **both** NPM Proxy Hosts
|
NPM-only Compose stack and recreate NPM with the managed `gitea` host alias
|
||||||
from `gitea:3000` to Atlas' HTTP endpoint. Do not change public DNS.
|
so **both** existing Proxy Hosts reach Atlas without changing their database
|
||||||
|
records. The old Gitea data stays intact. Do not change public DNS.
|
||||||
5. Test HTTPS login, representative clone/push, LFS, and public SSH clone/push
|
5. Test HTTPS login, representative clone/push, LFS, and public SSH clone/push
|
||||||
on port 2222 from outside the Atlas LAN. Record the last source write and
|
on port 2222 from outside the Atlas LAN. Record the last source write and
|
||||||
first healthy target service times; do not claim RPO/RTO without measuring.
|
first healthy target service times; do not claim RPO/RTO without measuring.
|
||||||
6. Only after successful traffic validation, remove Gitea from Prometheus'
|
6. After successful traffic validation, resume the Prometheus NPM-only backup
|
||||||
desired Compose stack and its backup-export path/container checks, leaving
|
export timer and verify its next result. Verify the next Atlas snapshot/Borg
|
||||||
NPM and its backups operational. Do not delete the old data. Verify the
|
run covers Gitea and test a restored target copy. Do not delete old source
|
||||||
next Atlas snapshot/Borg run covers Gitea and test a restored target copy.
|
data.
|
||||||
|
|
||||||
## Rollback gate
|
## Rollback gate
|
||||||
|
|
||||||
Before Atlas accepts writes, revert the two NPM routes, disable the public
|
Before Atlas accepts writes, restore the old Compose definition (removing the
|
||||||
2222 forward, and restart the unchanged source Gitea if target validation
|
NPM `gitea` host alias), disable the public 2222 proxy, and restart the
|
||||||
|
unchanged source Gitea if target validation
|
||||||
fails. **After Atlas accepts writes, do not blindly restart the source:** its
|
fails. **After Atlas accepts writes, do not blindly restart the source:** its
|
||||||
SQLite database and repositories are stale. Quiesce Atlas, capture its new
|
SQLite database and repositories are stale. Quiesce Atlas, capture its new
|
||||||
data, and decide a reverse migration or an extended outage explicitly.
|
data, and decide a reverse migration or an extended outage explicitly.
|
||||||
|
|||||||
Reference in New Issue
Block a user