diff --git a/AGENTS.md b/AGENTS.md index ca0f583..9bc63fe 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -25,6 +25,9 @@ Ansible-driven personal infrastructure repo for Fedora and Void desktops, Fedora - Preserve layering `all -> platform -> role -> desktop -> host`. - Keep `ansible/site.yml` small; orchestration belongs there, implementation belongs in roles. - Prefer minimal, targeted edits. Preserve idempotency and existing ordering. +- Keep completed one-time cleanup operations out of the playbook. Execute them directly + with explicit authorization; retain only the ongoing desired-state configuration and + historical documentation, not permanent cleanup flags or tasks. - Use Git Flow branch prefixes: `feature/` for new functionality, `bugfix/` for non-urgent fixes, `hotfix/` for urgent production fixes, `release/` for release preparation, and `support/` for maintained release lines. Do not use abbreviated prefixes such as `feat/`. @@ -61,6 +64,8 @@ Ansible-driven personal infrastructure repo for Fedora and Void desktops, Fedora `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 canonical Gitea domain (restarts only Gitea on a real configuration change): + `ansible-playbook ansible/site.yml --limit atlas --tags gitea_public_domain --check --diff` - Atlas iCloudPD storage and boot-started Quadlet: `ansible-playbook ansible/site.yml --limit atlas --tags icloudpd --check --diff` - Atlas explicit Gitea host-owner migration (live outage; never a normal run): @@ -94,7 +99,7 @@ Ansible-driven personal infrastructure repo for Fedora and Void desktops, Fedora `ansible-playbook ansible/site.yml --limit prometheus,aegis --tags wireguard --check --diff` - Prometheus NPM Quadlet steady state (does not perform a cutover): `ansible-playbook ansible/site.yml --limit prometheus --tags npm_quadlet --check --diff` - - DuckDNS config only: `ansible-playbook ansible/site.yml --limit prometheus --tags duckdns --check --diff` + - DuckDNS config only (skipped on Prometheus): `ansible-playbook ansible/site.yml --limit prometheus --tags duckdns --check --diff` ## Conventions - Use FQCN Ansible modules. @@ -138,7 +143,10 @@ The dotfile vars follow the same split: `desktop_common_dotfiles` carries mode-i - Windows applications are installed manually and are not managed from the WSL profile. ## Rocky Server Notes -- DuckDNS is rendered by `profile_server` from host-local `server_duckdns_domain` and +- Prometheus disables DuckDNS provisioning with `server_duckdns_enabled: false`. Its updater, + log and five-minute cron entry were explicitly retired; the external DuckDNS name and Vault + token remain untouched. The completed one-time cleanup has no remaining playbook tasks. +- When enabled, DuckDNS is rendered by `profile_server` from host-local `server_duckdns_domain` and `vault_duckdns_token`. Keep the rotated token in encrypted Vault or untracked local vars, never in dotfiles. The private `~/duckdns/duck.sh` keeps the existing entrypoint; rendering uses `no_log` and disables diffs. Provisioning does not execute the updater or change its external schedule. @@ -368,6 +376,18 @@ successfully. The first monthly scrub remains a runtime check. separate persistent application, database, and cache storage; keep credentials in Vault; publish it only through NPM over the Prometheus--Aegis gateway; and define backup, upgrade, and eventual Uranus-migration procedures before exposing user data. Do not deploy Nextcloud before the data-protection checklist is complete. +- [x] Move Gitea canonical HTTPS and SSH hostname to `git.fscotto.co` on + 2026-10-03 through Ansible. Only Gitea restarted; second run changed nothing. + HTTPS and authenticated SSH reads returned the same repository HEAD. + The new NPM hostnames passed TLS/HTTP checks; old DuckDNS Proxy Hosts were + observed disabled. Details are in `docs/domain-fscotto-co.md`. +- [ ] Confirm login on the new Gitea hostname and update remaining client remotes/integrations. + The operator confirmed SSH pull; transport/authentication work, but the agent did not test push. +- [x] Retire Prometheus' local DuckDNS updater on 2026-10-03 through Ansible: + the five-minute cron entry and private updater/log directory were removed. + Provisioning is disabled; repeat cleanup changed nothing. HTTPS services, private NPM + administration and the export timer stayed healthy. The external name and Vault token + remain untouched for possible future use on a local host. - [ ] Keep `atlas_manage_media_stack` disabled until the future Immich deployment has validated `/dev/dri`, container paths, and the required Vault database secret. diff --git a/README.it.md b/README.it.md index e6e5931..4195833 100644 --- a/README.it.md +++ b/README.it.md @@ -182,6 +182,10 @@ Le applicazioni Windows sono installate e gestite manualmente; il profilo WSL no ## Server +La migrazione dei servizi pubblici a `fscotto.co`, la gestione Ansible +degli URL Gitea e i passaggi ancora aperti per ritirare DuckDNS sono in +[`docs/domain-fscotto-co.md`](docs/domain-fscotto-co.md). + Sistema operativo: - Rocky Linux 9 @@ -227,7 +231,11 @@ Mantenere l'autenticazione Syncthing e una policy di accesso NPM adeguata. ### DuckDNS -`profile_server` genera `~/duckdns/duck.sh` con permessi `0700`, mantenendo il percorso dello +`server_duckdns_enabled: false` disabilita il provisioning su Prometheus, che usa IP statico +e `fscotto.co`. Updater, log e cron ogni cinque minuti sono stati rimossi una sola volta; +non restano task o flag di pulizia. Il nome DuckDNS esterno e il token Vault restano invariati. + +Sui server con `server_duckdns_enabled: true`, `profile_server` genera `~/duckdns/duck.sh` con permessi `0700`, mantenendo il percorso dello script e `duck.log`. Definire `server_duckdns_domain` negli host vars del server e salvare il **nuovo token rigenerato** in `vault_duckdns_token`, nel Vault cifrato `secrets/vault.yml` (`ansible-vault edit secrets/vault.yml`) oppure negli override non versionati `secrets/vault.local.yml`. diff --git a/README.md b/README.md index cbbc7e8..b48853d 100644 --- a/README.md +++ b/README.md @@ -125,6 +125,10 @@ That gives it Fedora packages through DNF, Docker from the official repository, ## Server +The public service domain transition to `fscotto.co`, Gitea canonical URL +management, and remaining DuckDNS retirement steps are documented in +[`docs/domain-fscotto-co.md`](docs/domain-fscotto-co.md). + `prometheus` is the Rocky Linux 9 server. It has no graphical environment and gets server-specific dotfiles and templates. The profile does not transfer application data, update DNS, or perform an implicit service cutover. @@ -167,7 +171,11 @@ Prometheus authorizes its declared SSH public keys through separate files below ### DuckDNS -`profile_server` renders `~/duckdns/duck.sh` with mode `0700`, keeping the existing updater path +`server_duckdns_enabled: false` disables provisioning on Prometheus, which uses its static IP +and `fscotto.co`. The local updater, log and five-minute cron job were removed once; +no cleanup tasks or flags remain. The external DuckDNS name and Vault token remain untouched. + +For servers with `server_duckdns_enabled: true`, `profile_server` renders `~/duckdns/duck.sh` with mode `0700`, keeping the existing updater path and `duck.log`. Set `server_duckdns_domain` in the server's host vars and store the **rotated** `vault_duckdns_token` in encrypted `secrets/vault.yml` (using `ansible-vault edit secrets/vault.yml`) or untracked `secrets/vault.local.yml`. Never commit the rendered script or put the token on a diff --git a/ansible/inventory/group_vars/server.yml b/ansible/inventory/group_vars/server.yml index d3f22e6..fa2e1d3 100644 --- a/ansible/inventory/group_vars/server.yml +++ b/ansible/inventory/group_vars/server.yml @@ -10,6 +10,7 @@ server_npm_quadlet_stage: false server_npm_quadlet_cutover: false server_legacy_stack_retired: false server_legacy_cleanup: false +server_duckdns_enabled: true ai_agents: {} vim_plugins_enabled: false diff --git a/ansible/inventory/host_vars/atlas.yml b/ansible/inventory/host_vars/atlas.yml index e5d63fe..39b8694 100644 --- a/ansible/inventory/host_vars/atlas.yml +++ b/ansible/inventory/host_vars/atlas.yml @@ -52,6 +52,7 @@ atlas_manage_storage: true # Rootless Gitea was restored from the stopped-source export before production activation. atlas_manage_gitea: true atlas_gitea_production_enabled: true +atlas_gitea_public_domain: git.fscotto.co atlas_prometheus_pull_start_timer: true atlas_manage_zfs_snapshots: true atlas_zfs_snapshot_prefix: atlas-auto diff --git a/ansible/inventory/host_vars/prometheus.yml b/ansible/inventory/host_vars/prometheus.yml index e14d525..7014f39 100644 --- a/ansible/inventory/host_vars/prometheus.yml +++ b/ansible/inventory/host_vars/prometheus.yml @@ -27,6 +27,7 @@ server_gitea_on_atlas: true server_gitea_npm_domains: - git.fscotto.duckdns.org server_duckdns_domain: fscotto +server_duckdns_enabled: false server_ssh_authorized_keys: - name: ikaros key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINrIxXjA3ffPwziKGR5gzc4gAoBehQPlnEMcXF4Wl0ZS ikaros" diff --git a/ansible/roles/profile_atlas/defaults/main.yml b/ansible/roles/profile_atlas/defaults/main.yml index 19edfc9..0248fc1 100644 --- a/ansible/roles/profile_atlas/defaults/main.yml +++ b/ansible/roles/profile_atlas/defaults/main.yml @@ -184,6 +184,7 @@ atlas_gitea_quadlet_dir: "{{ atlas_gitea_home }}/.config/containers/systemd" atlas_gitea_image: localhost/atlas-gitea:1.25.2-user-gitea-v1 atlas_gitea_image_build_dir: "{{ atlas_gitea_home }}/.local/share/atlas-gitea-image" atlas_gitea_production_enabled: false +atlas_gitea_public_domain: "" atlas_gitea_bind_address: "{{ ansible_host }}" atlas_gitea_http_port: 3000 atlas_gitea_ssh_port: 2222 diff --git a/ansible/roles/profile_atlas/tasks/gitea_public_domain.yml b/ansible/roles/profile_atlas/tasks/gitea_public_domain.yml new file mode 100644 index 0000000..47cb5a3 --- /dev/null +++ b/ansible/roles/profile_atlas/tasks/gitea_public_domain.yml @@ -0,0 +1,58 @@ +--- +- name: Manage the public domain of the restored production Gitea + tags: [atlas, gitea, gitea_public_domain] + when: + - atlas_manage_gitea | bool + - atlas_gitea_production_enabled | bool + - atlas_gitea_public_domain | length > 0 + block: + - name: Require an explicit public Gitea hostname + ansible.builtin.assert: + that: + - atlas_gitea_public_domain is match('^[a-zA-Z0-9][a-zA-Z0-9.-]*\.[a-zA-Z]{2,}$') + + - name: Inspect the restored private Gitea configuration + ansible.builtin.stat: + path: "{{ atlas_gitea_mountpoint }}/config/app.ini" + follow: false + register: atlas_gitea_public_config + + - name: Refuse to create or replace an unprepared Gitea configuration + ansible.builtin.assert: + that: + - atlas_gitea_public_config.stat.isreg | default(false) + - atlas_gitea_public_config.stat.uid | int == atlas_gitea_uid | int + - atlas_gitea_public_config.stat.mode == '0600' + + # app.ini contains secrets: preserve all unrelated settings and suppress diffs. + - name: Set only the declared public Gitea server fields + community.general.ini_file: + path: "{{ atlas_gitea_mountpoint }}/config/app.ini" + section: server + option: "{{ item.option }}" + value: "{{ item.value }}" + create: false + backup: true + owner: "{{ atlas_gitea_username }}" + group: "{{ atlas_gitea_group }}" + mode: "0600" + loop: + - { option: DOMAIN, value: "{{ atlas_gitea_public_domain }}" } + - { option: ROOT_URL, value: "https://{{ atlas_gitea_public_domain }}/" } + - { option: SSH_DOMAIN, value: "{{ atlas_gitea_public_domain }}" } + register: atlas_gitea_public_domain_update + no_log: true + diff: false + + - name: Restart only Gitea when its public configuration changes + become_user: "{{ atlas_gitea_username }}" + ansible.builtin.systemd: + name: atlas-gitea.service + scope: user + state: restarted + environment: + XDG_RUNTIME_DIR: "/run/user/{{ atlas_gitea_uid }}" + DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ atlas_gitea_uid }}/bus" + when: + - atlas_gitea_public_domain_update is changed + - not ansible_check_mode diff --git a/ansible/roles/profile_atlas/tasks/main.yml b/ansible/roles/profile_atlas/tasks/main.yml index 259059b..77f550a 100644 --- a/ansible/roles/profile_atlas/tasks/main.yml +++ b/ansible/roles/profile_atlas/tasks/main.yml @@ -20,6 +20,9 @@ - name: Import staged Atlas rootless Gitea tasks ansible.builtin.import_tasks: gitea.yml +- name: Import the declared Atlas Gitea public domain + ansible.builtin.import_tasks: gitea_public_domain.yml + - name: Import Atlas iCloudPD storage and boot-started Quadlet tasks ansible.builtin.import_tasks: icloudpd.yml diff --git a/ansible/roles/profile_server/tasks/main.yml b/ansible/roles/profile_server/tasks/main.yml index fb9450c..e53f3da 100644 --- a/ansible/roles/profile_server/tasks/main.yml +++ b/ansible/roles/profile_server/tasks/main.yml @@ -11,6 +11,7 @@ - name: Configure DuckDNS updater tags: [dotfiles, dotfiles:server, duckdns] ansible.builtin.import_tasks: duckdns.yml + when: server_duckdns_enabled | bool - name: Ensure server directories exist tags: [dotfiles, services] diff --git a/docs/atlas-gitea-migration.md b/docs/atlas-gitea-migration.md index 25215f6..987fe9a 100644 --- a/docs/atlas-gitea-migration.md +++ b/docs/atlas-gitea-migration.md @@ -1,5 +1,10 @@ # Gitea migration from Prometheus to Atlas +The later 2026-10-03 canonical-domain change to `git.fscotto.co` is recorded +in `docs/domain-fscotto-co.md`. Public SSH remains on TCP/2222; the old +DuckDNS Proxy Host was observed disabled. Earlier domain references below +describe migration evidence, not the current canonical URL. + This records the staged migration and its observed partial cutover. Gitea is temporary on Atlas until Uranus; NPM remains on Prometheus. On 2026-10-03 the operator explicitly approved removal of the old Prometheus Gitea data, diff --git a/docs/domain-fscotto-co.md b/docs/domain-fscotto-co.md new file mode 100644 index 0000000..dc8342f --- /dev/null +++ b/docs/domain-fscotto-co.md @@ -0,0 +1,79 @@ +# fscotto.co domain transition + +## Observed state (2026-10-03) + +Namecheap remains the DNS provider. The operator moved GitHub Pages to +`blog.fscotto.co` in `fscotto/fscotto.github.io`, aligned Hugo and Pages +settings, and changed the apex A record to `179.237.102.172`. The blog +remains a CNAME to `fscotto.github.io`; mail records were left unchanged. +A new Hugo deployment and cache clearing resolved the initial stale DNS +and generated URLs. Blog HTTPS returned 200 with valid TLS. + +The `git`, `music` and `syncthing` subdomains are CNAMEs to `fscotto.co`. +The operator added NPM Proxy Hosts with certificates, WebSocket support +and Force SSL: + +| Hostname | HTTP upstream | +| --- | --- | +| git.fscotto.co | 192.168.178.55:3000 | +| music.fscotto.co | 192.168.178.55:4533 | +| syncthing.fscotto.co | 192.168.178.55:8384 | + +All three redirected HTTP to HTTPS and returned final HTTPS 200 with valid +TLS. Only the Syncthing GUI uses NPM; native synchronization is unchanged. +NPM administration remains loopback-only on port 81 via SSH tunnel. + +## Gitea canonical hostname + +Atlas declares `atlas_gitea_public_domain: git.fscotto.co`. Ansible manages +only `[server] DOMAIN`, `ROOT_URL` and `SSH_DOMAIN` in the existing private +app.ini, preserving unrelated settings and mode 0600. Private configuration +backups are created; diffs and secret-bearing results are suppressed. +Only Gitea restarts when these fields change; a repeat run changed nothing. + +HTTPS uses `https://git.fscotto.co/`; public SSH remains TCP/2222. +Agent read-only checks returned the same HEAD from `fscotto/infra.git` +over HTTPS and authenticated SSH. SSH host identity was checked against +the already-trusted old endpoint key. No test push or user-authenticated +web login was performed by the agent. + +```bash +ansible-playbook ansible/site.yml --limit atlas --tags gitea_public_domain --check --diff +``` + +Client remotes do not update automatically. Update them deliberately after +checking repository paths; integrations and webhooks are separate operations. +For the verified infrastructure repository only: + +```bash +git remote set-url origin ssh://git@git.fscotto.co:2222/fscotto/infra.git +``` + +Do not copy this path into unrelated clones. Verify Gitea's known SSH key +before accepting the new hostname's identity. + +## Local DuckDNS retirement + +Prometheus declares `server_duckdns_enabled: false`. On 2026-10-03 the explicit +Ansible cleanup removed the five-minute rocky cron entry and the private +`~/duckdns` directory containing only `duck.sh` and `duck.log`. The temporary +cleanup tasks and flag were subsequently removed from the playbook at the +operator's request. Only the disabled provisioning state remains; ordinary +provisioning cannot recreate the updater. +The external DuckDNS name, Vault token, disabled NPM hosts and certificates +remain untouched for a separate future decision. +The repeat cleanup changed nothing; ordinary DuckDNS provisioning was skipped. +The cron table had no remaining entries, NPM and the export timer were active, +and NPM administration still listened only on `127.0.0.1:81`. + +## Remaining transition work + +- Confirm user-authenticated login and push on the new Gitea hostname. +- Update existing remotes and callback/webhook URLs explicitly. +- Retire obsolete NPM hosts/certificates and the old upstream override + after confirming they are no longer needed. + +At inspection the three old DuckDNS Proxy Hosts were already disabled, +not deleted. They are not working HTTPS rollback endpoints. Existing backup +archives remain preserved. DNS/Pages/NPM changes were operator actions; +the Gitea application configuration change was deployed through Ansible.