This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

Infra

Infrastructure tree

This tree contains infrastructure as code. Tracked source follows the repository’s public-source policy. Infrastructure facts are not confidential merely because they are operational, generated, or live. Reports may include them unless they contain credentials, other secrets, or personal information. Inspect raw state, plans, inventories, and decrypted configuration because those artifacts can contain prohibited content; do not track the artifacts themselves.

Each infrastructure project owns its specifications and maintained changes in <project>/openspec/. Use the pinned OpenSpec workflow with that project selected. These specifications describe checked-in definitions; they do not establish deployed state or authorize infrastructure operations.

  • Bazel targets MUST use repository-internal visibility.
  • Infrastructure definitions MUST NOT be published as production artifacts.
  • Infrastructure targets MUST NOT be dependencies of production build targets.
  • Public checked-in documentation MAY be included in the repository documentation site, including non-secret, non-personal operational facts.

infra.al_lib.xo_login({ labels = ..., oidc_provider = ... }) registers the Xen Orchestra login plugin with the shared XO and Vault endpoints. The caller must supply the Vault OIDC provider name; the helper constructs its discovery URL. Consumers include //infra/xcp_ng/cmd/xo_login in their runtime data and select the same labels in their command wrapper. Registration does not authenticate until a matching command selects the plugin.

The commands below are state-changing operator examples. An agent must use bazel_agent, apply the repository Terraform and secret-handling procedures, and receive explicit authority for the exact operation and environment before running an equivalent command.

  • Create an approle: example
  • Add it to approles: example
  • Run apply:
    bazel_agent bazel run //infra/vault/tf:tf.apply
    
  • Update Yandex Cloud folders:
    bazel_agent bazel run //infra/yandex_cloud/org1/tf:tf.apply
    
  • Update Proxmox resource pools:
    bazel_agent bazel run //infra/pve/tf:tf.apply
    
  • Update Xen Orchestra AppRole resource sets:
    bazel_agent bazel run //infra/xcp_ng/tf:tf.apply
    
  • Set up al config: example
  • Configure the component’s Terraform state through the existing Vault HTTP backend flow. The tf_backend plugin creates state and lock KV entries on first use.

1 - Archived infrastructure

Historical architectures without maintained component projects

This project preserves the archived Harvester, Opencode, and Hermes diagrams. They describe historical source documents, not current inventory or service availability. Archived diagrams with existing projects remain with those owners: Proxmox and Harbor.

This archived diagram preserves the historical source topology. The Mermaid source is authoritative; Bazel renders its light and dark SVGs.

Archive/Harvester architectureArchive/Harvester architecture

This archived diagram preserves the historical source topology. The Mermaid source is authoritative; Bazel renders its light and dark SVGs.

Archive/Opencode architectureArchive/Opencode architecture

This archived diagram preserves the historical source topology. The Mermaid source is authoritative; Bazel renders its light and dark SVGs.

Archive/Hermes architectureArchive/Hermes architecture

2 - Ceph

Ceph

Go to Nodes -> Host -> Ceph -> Configuration

Ssh to the PVE host and update the map:

sudo ceph osd getcrushmap -o crushmap.cm
sudo crushtool --decompile crushmap.cm -o crushmap.txt
sudo vim crushmap.txt
sudo crushtool --compile crushmap.txt -o new_crushmap.cm
sudo ceph osd setcrushmap -i new_crushmap.cm
sudo ceph -s

2.1 - OpenSpec

2.1.1 - Ceph infrastructure specification

Describe the single-host Ceph CRUSH map maintained by infra/ceph and its documented operator workflow. This owner contains a map and documentation; its BUILD file exposes documentation, not a Ceph deployment target. The baseline describes checked-in source, not an observed running cluster.

Baseline revision: 550d7e79b1f5fdbc2b6017b75178471d6914082f. Observed: 2026-09-08. Sources: owner README, BUILD, and CRUSH map.

The maintained CRUSH map SHALL place four HDD devices, osd.0 through osd.3, in the host1 bucket beneath the default root, using the declared device weights and straw2 bucket algorithm.

  • WHEN a maintainer reads the checked-in CRUSH map
  • THEN all four OSDs belong to host1, and the default root contains that host
  • AND this topology establishes a single-host placement baseline without asserting that the running cluster matches it

The maintained map SHALL define a replicated rule using chooseleaf firstn 0 type osd and an erasure rule using chooseleaf indep 0 type osd beneath the default root. The erasure rule SHALL set chooseleaf attempts to 5 and choose attempts to 100.

  • WHEN a maintainer evaluates replicated_rule or ceph-ec-data
  • THEN placement selects OSDs within the recorded topology
  • AND the map provides no requirement for replica separation across hosts

The owner documentation SHALL describe exporting, decompiling, editing, compiling, and installing a CRUSH map followed by a Ceph status check. Executing that workflow MUST remain subject to the repository’s explicit infrastructure-operation authorization requirement.

  • WHEN an authorized operator follows the map update instructions
  • THEN the procedure obtains the current map before creating its compiled replacement and checks ceph -s after installation
  • AND the presence of these instructions alone does not authorize a live map update

3 - Cloud-init

Shared PVE-based infrastructure VM bootstrap configuration

assets/cloud_init.yaml is the canonical configuration moved from PVE. PVE and Yandex Cloud consume it directly. It owns the Ansible sudo user, SSH keys, local TLS root CA, and common bootstrap policy. The Ansible SSH CA is selected by its certificate-authority principal options; user and key ordering do not affect the Xen configuration. Missing or duplicate Ansible users or CA keys fail template rendering. assets/cloud_init_min.yaml is the existing minimal PVE configuration with QEMU guest tools. Both original PVE files retain their contents.

The xen_linux target uses the repository’s rules_template Go template rule to derive xen_linux.json from that same PVE configuration. Its small template selects the shared Ansible CA key, uses Fedora’s users,wheel groups, and adds Xen guest tools. It omits PVE’s explicit package-reboot and public metadata-key flags to preserve the existing Xen bootstrap behavior. All other base fields, including the TLS CA, come from the PVE configuration. There is no separate copy of the Xen user or CA configuration and no Terraform template file.

Forgejo merges its hostname into the generated object and uses Terraform’s yamlencode to serialize it. Its static network structure stays in Terraform, where interface, address, MAC, gateway, and DNS are runtime values. The rendered Forgejo user-data and network configuration remain byte-identical to the previous configuration.

PVE Ansible packages the moved snippets under the existing files/cloud_init.yaml and files/cloud_init_min.yaml names. Those packaging aliases preserve local:snippets/cloud_init.yaml references on PVE hosts. Subsequent persistent VM configuration belongs in Ansible. Targets are repository-internal runtime inputs, not published artifacts.

4 - Dns

Project-owned DNS declarations for alwaldend.com

DNS records live in each owner’s dnsconfig.json. Each owner manages its records through the reusable Terraform DNS module from its designated tf_setup or tf root. This component’s dnsconfig.json owns shared apex and mail records.

This diagram describes the maintained architecture source, not live inventory or service health. The Mermaid source is authoritative; Bazel renders its light and dark SVGs.

DNS architectureDNS architecture

Cloudflare DNS and Mikrotik DNS list the records this repository declares for each destination view, with the owning declaration for every record. They are manually refreshed snapshots of dnsconfig.json and may lag behind the declarations. Regenerate them explicitly when a documentation refresh is wanted, for example after parallel DNS changes have merged:

bazel_agent bazel run //infra/dns/cmd/dump -- --write

Ordinary validation checks declarations and ownership without requiring or rewriting these snapshots. A DNS change does not require a documentation refresh in the same PR. To explicitly check snapshot freshness, run bazel_agent bazel run //infra/dns/cmd/dump -- --check.

bazel_agent bazel run //infra/dns:lint

The linter discovers dnsconfig.json files in the current workspace at runtime and prints their DNS declarations as a table. It includes nested project workspaces and excludes task scratch, tool caches, and generated directories. There is no central source registry to maintain when a project adds a file.

A canonical domain name must belong to one source file. Multiple values, record types, and destination views within that file are valid. Duplicate JSON object keys and domains declared by different source files are errors. The command reads checked-in declarations without contacting Terraform backends or DNS providers.

bazel_agent bazel test //infra/dns:config_test runs the linter’s fixture tests and checks every DNS declaration in the current checkout. The repository test resolves the root MODULE.bazel runfile to the checkout and runs without sandboxing or cached test results so newly added files and nested workspaces are included on every run. It only reads local files.

The provider-free normalization module owns the Terraform input schema, relative-name handling, TTL defaults, and stable resource keys. Each logical key can contain several record-type members; dsp selects global, dc1, or all. Explicit member TTLs preserve shared apex and mail settings. The normalization module remains an internal dependency of each owner’s Terraform DNS module.

Owner-local roots receive their provider credentials and Vault HTTP state backend through the existing AL configuration. The shared module resolves the Cloudflare zone by name when no optional zone ID is provided. DNS and ingress reuse the RouterOS endpoint declared in the shared AL configuration, while their separate credentials remain in Vault. Neither integration requires adding metadata fields to the existing credential entries. Validate their source with the owning package’s offline checks. A live plan, import, apply, or state operation requires separate authorization for that owner and operation.

Service roots expose dns.plan, dns.show, and dns.apply alongside their ordinary Terraform commands. They retain the owning root, AppRole and backend, select the DNS AL calls, and plan module.dns. Required root inputs still load through the existing read-only Vault injectors. Unrelated service authentication is not selected. Apply accepts only a reviewed saved plan file and rejects Terraform CLI argument environment overrides. See the runbook for the optional import-ID maps and adoption checks.

The migration change and cutover runbook describe adoption, imports, recovery, and retirement requirements. Each owner’s adoption change records its verified live transfer; source implementation alone does not establish operational ownership. The completed coordination record links the operational acceptance evidence and its coverage limits.

Current source no longer exposes the central DNSControl deployment commands. Before an owner begins Terraform reconciliation, stop central deployment jobs identified by the active and scheduled writer audit. Record coverage and unavailable observations, and coordinate one owner at a time. Adopt existing records into the owner’s state with an exact-ID, no-change plan. Fresh complete inventories may establish missing declarations for a separately reviewed additions-only plan that preserves all existing records. Rollback requires stopping the affected Terraform writers and following the runbook’s recorded prior revision and state-reconciliation procedure.

Deployment snapshots are not maintained here. The declaration pages above reflect their last manual refresh; each owner’s declarations remain the authoritative inventory.

4.1 - Cloudflare DNS

Records declared for the public zone served by Cloudflare

Every record this repository declares for the public zone, which Cloudflare serves.

This page is generated from the declarations; run bazel run //infra/dns/cmd/dump -- --write after changing them. The authoritative source is each owner’s dnsconfig.json.

Domain Type TTL Value Declaration
@ A 300 185.199.108.153 infra/dns/dnsconfig.json
@ A 300 185.199.109.153 infra/dns/dnsconfig.json
@ A 300 185.199.110.153 infra/dns/dnsconfig.json
@ A 300 185.199.111.153 infra/dns/dnsconfig.json
@ AAAA 300 2606:50c0:8000::153 infra/dns/dnsconfig.json
@ AAAA 300 2606:50c0:8001::153 infra/dns/dnsconfig.json
@ AAAA 300 2606:50c0:8002::153 infra/dns/dnsconfig.json
@ AAAA 300 2606:50c0:8003::153 infra/dns/dnsconfig.json
@ MX 300 10 mail.protonmail.ch. infra/dns/dnsconfig.json
@ MX 300 20 mailsec.protonmail.ch. infra/dns/dnsconfig.json
@ TXT 300 _globalsign-domain-verification=0QBJgVV_uwcFLTi1Rot3bb1LyJ5uW1WD0ygvIS4OM5 infra/dns/dnsconfig.json
@ TXT 300 protonmail-verification=bdcd133d3f472fa17f66328950d02fbeae1bef75 infra/dns/dnsconfig.json
@ TXT 300 v=spf1 include:_spf.protonmail.ch ~all infra/dns/dnsconfig.json
_dmarc TXT 300 v=DMARC1; p=quarantine; adkim=s infra/dns/dnsconfig.json
_dmarc.simplelogin TXT 10800 v=DMARC1; p=quarantine; pct=100; adkim=s; aspf=s infra/dns/dnsconfig.json
canvas.openhands CNAME default ingress.alwaldend.com. infra/openhands/dnsconfig.json
cloud CNAME default ingress.alwaldend.com. infra/nas/dnsconfig.json
dc1.automation.openhands A default 192.168.10.92 infra/openhands/dnsconfig.json
dc1.canvas.openhands A default 192.168.10.90 infra/openhands/dnsconfig.json
dc1.cloud A default 192.168.1.209 infra/nas/dnsconfig.json
dc1.host-bot.simeonwarren.users A default 192.168.1.210 users/simeonwarren/host_bot/dnsconfig.json
dc1.server.openhands A default 192.168.10.91 infra/openhands/dnsconfig.json
dc1.t3code.host-bot.simeonwarren.users A default 192.168.1.210 users/simeonwarren/host_bot/dnsconfig.json
dkim._domainkey.simplelogin CNAME 10800 dkim._domainkey.simplelogin.co. infra/dns/dnsconfig.json
dkim02._domainkey.simplelogin CNAME 10800 dkim02._domainkey.simplelogin.co. infra/dns/dnsconfig.json
dkim03._domainkey.simplelogin CNAME 10800 dkim03._domainkey.simplelogin.co. infra/dns/dnsconfig.json
forgejo CNAME default ingress.alwaldend.com. infra/forgejo/dnsconfig.json
git CNAME default ingress.alwaldend.com. infra/forgejo/dnsconfig.json
host1.ingress A default 81.26.185.118 infra/ingress/dnsconfig.json
ingress A default 81.26.185.118 infra/ingress/dnsconfig.json
int.forgejo A default 192.168.10.40 infra/forgejo/dnsconfig.json
int.vault A default 192.168.1.218 infra/vault/dnsconfig.json
mail._domainkey.yandex TXT 300 v=DKIM1; k=rsa; t=s; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCcYzFVgkeDOhaIIkWM8gNQjxVsv0/aXfU+ax5urB5y6hA6lSjRnjRo6tm0bXbkOJf41GmiwMNgdXpwRtzgzAlX1i2aJbtEr4b9jzibEGLQ7Cvqs44bOYES9f/K3ueQpnvdTOJmFqlRReFL7ZrUyDFCoQ7f4+7h4i8s01cCcRrt5wIDAQAB infra/dns/dnsconfig.json
njalla1.nodes.threexui A default 45.142.141.133 infra/threexui/dnsconfig.json
njalla1.nodes.threexui AAAA default 2a0a:3840:8078:141:0:2d8e:8d85:1337 infra/threexui/dnsconfig.json
pages A default 185.199.108.153 projects/alwaldend.com/dnsconfig.json
pages AAAA default 2606:50c0:8000::153 projects/alwaldend.com/dnsconfig.json
protonmail._domainkey CNAME 300 protonmail.domainkey.djgwfzcu5fgjtpoijqqomgifmqj6zeiuwdd4mzim4hrxab3zsgwkq.domains.proton.ch. infra/dns/dnsconfig.json
protonmail2._domainkey CNAME 300 protonmail2.domainkey.djgwfzcu5fgjtpoijqqomgifmqj6zeiuwdd4mzim4hrxab3zsgwkq.domains.proton.ch. infra/dns/dnsconfig.json
protonmail3._domainkey CNAME 300 protonmail3.domainkey.djgwfzcu5fgjtpoijqqomgifmqj6zeiuwdd4mzim4hrxab3zsgwkq.domains.proton.ch. infra/dns/dnsconfig.json
simplelogin MX 10800 10 mx1.simplelogin.co. infra/dns/dnsconfig.json
simplelogin MX 10800 20 mx2.simplelogin.co. infra/dns/dnsconfig.json
simplelogin TXT 10800 sl-verification=bxfzzfjiggzsxyzxhhmkmjqkaskjgy infra/dns/dnsconfig.json
simplelogin TXT 10800 v=spf1 include:simplelogin.co ~all infra/dns/dnsconfig.json
t3code.host-bot.simeonwarren.users CNAME default ingress.alwaldend.com. users/simeonwarren/host_bot/dnsconfig.json
vault CNAME default ingress.alwaldend.com. infra/vault/dnsconfig.json
www CNAME 300 alwaldend.com. infra/dns/dnsconfig.json
www-staging CNAME 300 alwaldend.github.io. infra/dns/dnsconfig.json
yandex MX 21600 10 mx.yandex.net. infra/dns/dnsconfig.json
yandex TXT 300 v=spf1 redirect=_spf.yandex.net infra/dns/dnsconfig.json
yandex TXT 300 yandex-verification: b83672f59b3dbe16 infra/dns/dnsconfig.json
yc.threexui NS default ns1.yandexcloud.net. infra/threexui/dnsconfig.json
yc.threexui NS default ns2.yandexcloud.net. infra/threexui/dnsconfig.json
yc1.nodes.threexui CNAME default host1.nodes.yc.threexui.alwaldend.com. infra/threexui/dnsconfig.json

4.2 - Mikrotik DNS

Records declared for the internal dc1 view served by RouterOS

Every record this repository declares for the internal dc1 view, which RouterOS serves for that network.

This page is generated from the declarations; run bazel run //infra/dns/cmd/dump -- --write after changing them. The authoritative source is each owner’s dnsconfig.json.

Domain Type TTL Value Declaration
@ A 300 185.199.108.153 infra/dns/dnsconfig.json
@ A 300 185.199.109.153 infra/dns/dnsconfig.json
@ A 300 185.199.110.153 infra/dns/dnsconfig.json
@ A 300 185.199.111.153 infra/dns/dnsconfig.json
@ AAAA 300 2606:50c0:8000::153 infra/dns/dnsconfig.json
@ AAAA 300 2606:50c0:8001::153 infra/dns/dnsconfig.json
@ AAAA 300 2606:50c0:8002::153 infra/dns/dnsconfig.json
@ AAAA 300 2606:50c0:8003::153 infra/dns/dnsconfig.json
@ MX 300 10 mail.protonmail.ch. infra/dns/dnsconfig.json
@ MX 300 20 mailsec.protonmail.ch. infra/dns/dnsconfig.json
@ TXT 300 _globalsign-domain-verification=0QBJgVV_uwcFLTi1Rot3bb1LyJ5uW1WD0ygvIS4OM5 infra/dns/dnsconfig.json
@ TXT 300 protonmail-verification=bdcd133d3f472fa17f66328950d02fbeae1bef75 infra/dns/dnsconfig.json
@ TXT 300 v=spf1 include:_spf.protonmail.ch ~all infra/dns/dnsconfig.json
_dmarc TXT 300 v=DMARC1; p=quarantine; adkim=s infra/dns/dnsconfig.json
_dmarc.simplelogin TXT 10800 v=DMARC1; p=quarantine; pct=100; adkim=s; aspf=s infra/dns/dnsconfig.json
automation.openhands A default 192.168.10.92 infra/openhands/dnsconfig.json
bm1.dc1 A default 192.168.1.222 infra/mikrotik/dnsconfig.json
bm2.dc1 A default 192.168.1.216 infra/pve/dnsconfig.json
bm2.dc1 AAAA default fd2e:546d:5738:0:365a:60ff:fe08:6daa infra/pve/dnsconfig.json
bm3.dc1 A default 192.168.1.218 infra/vault/dnsconfig.json
bm3.dc1 AAAA default fd2e:546d:5738:0:e2be:3ff:fe2b:9a1a infra/vault/dnsconfig.json
canvas.openhands A default 192.168.10.90 infra/openhands/dnsconfig.json
cloud CNAME default nas.alwaldend.com. infra/nas/dnsconfig.json
cloudinit-test.vm.pve1.dc1 A default 192.168.10.10 infra/pve/dnsconfig.json
dc1.automation.openhands A default 192.168.10.92 infra/openhands/dnsconfig.json
dc1.canvas.openhands A default 192.168.10.90 infra/openhands/dnsconfig.json
dc1.cloud A default 192.168.1.209 infra/nas/dnsconfig.json
dc1.host-bot.simeonwarren.users A default 192.168.1.210 users/simeonwarren/host_bot/dnsconfig.json
dc1.server.openhands A default 192.168.10.91 infra/openhands/dnsconfig.json
dc1.t3code.host-bot.simeonwarren.users A default 192.168.1.210 users/simeonwarren/host_bot/dnsconfig.json
dkim._domainkey.simplelogin CNAME 10800 dkim._domainkey.simplelogin.co. infra/dns/dnsconfig.json
dkim02._domainkey.simplelogin CNAME 10800 dkim02._domainkey.simplelogin.co. infra/dns/dnsconfig.json
dkim03._domainkey.simplelogin CNAME 10800 dkim03._domainkey.simplelogin.co. infra/dns/dnsconfig.json
flux A default 192.168.10.60 infra/flux/dnsconfig.json
forgejo A default 192.168.10.40 infra/forgejo/dnsconfig.json
git A default 192.168.10.40 infra/forgejo/dnsconfig.json
harbor A default 192.168.10.50 infra/harbor/dnsconfig.json
host-bot.simeonwarren.users A default 192.168.1.210 users/simeonwarren/host_bot/dnsconfig.json
host1.automation.openhands A default 192.168.10.92 infra/openhands/dnsconfig.json
host1.canvas.openhands A default 192.168.10.90 infra/openhands/dnsconfig.json
host1.cloud CNAME default host1.nas.alwaldend.com. infra/nas/dnsconfig.json
host1.flux A default 192.168.10.60 infra/flux/dnsconfig.json
host1.forgejo A default 192.168.10.40 infra/forgejo/dnsconfig.json
host1.harbor A default 192.168.10.50 infra/harbor/dnsconfig.json
host1.ingress A default 81.26.185.118 infra/ingress/dnsconfig.json
host1.nas A default 192.168.1.209 infra/nas/dnsconfig.json
host1.pve1.dc1 CNAME default bm2.dc1.alwaldend.com. infra/pve/dnsconfig.json
host1.server.openhands A default 192.168.10.91 infra/openhands/dnsconfig.json
host1.threexui A default 192.168.10.80 infra/threexui/dnsconfig.json
host1.vault.dc1 A default 192.168.1.218 infra/vault/dnsconfig.json
host1.xcp-ng A default 192.168.1.213 infra/xcp_ng/dnsconfig.json
host1.xoa.xcp-ng A default 192.168.1.206 infra/xcp_ng/dnsconfig.json
ingress A default 81.26.185.118 infra/ingress/dnsconfig.json
mail._domainkey.yandex TXT 300 v=DKIM1; k=rsa; t=s; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCcYzFVgkeDOhaIIkWM8gNQjxVsv0/aXfU+ax5urB5y6hA6lSjRnjRo6tm0bXbkOJf41GmiwMNgdXpwRtzgzAlX1i2aJbtEr4b9jzibEGLQ7Cvqs44bOYES9f/K3ueQpnvdTOJmFqlRReFL7ZrUyDFCoQ7f4+7h4i8s01cCcRrt5wIDAQAB infra/dns/dnsconfig.json
nas A default 192.168.1.209 infra/nas/dnsconfig.json
njalla1.nodes.threexui A default 45.142.141.133 infra/threexui/dnsconfig.json
njalla1.nodes.threexui AAAA default 2a0a:3840:8078:141:0:2d8e:8d85:1337 infra/threexui/dnsconfig.json
openid.flux CNAME default flux.alwaldend.com. infra/flux/dnsconfig.json
operator.flux CNAME default flux.alwaldend.com. infra/flux/dnsconfig.json
protonmail._domainkey CNAME 300 protonmail.domainkey.djgwfzcu5fgjtpoijqqomgifmqj6zeiuwdd4mzim4hrxab3zsgwkq.domains.proton.ch. infra/dns/dnsconfig.json
protonmail2._domainkey CNAME 300 protonmail2.domainkey.djgwfzcu5fgjtpoijqqomgifmqj6zeiuwdd4mzim4hrxab3zsgwkq.domains.proton.ch. infra/dns/dnsconfig.json
protonmail3._domainkey CNAME 300 protonmail3.domainkey.djgwfzcu5fgjtpoijqqomgifmqj6zeiuwdd4mzim4hrxab3zsgwkq.domains.proton.ch. infra/dns/dnsconfig.json
pve A default 192.168.1.216 infra/pve/dnsconfig.json
router1.dc1 A default 192.168.1.1 infra/mikrotik/dnsconfig.json
router1.dc1 AAAA default fd2e:546d:5738::1 infra/mikrotik/dnsconfig.json
runner1.forgejo-runner A default 192.168.10.100 infra/forgejo_runner/dnsconfig.json
server.openhands A default 192.168.10.91 infra/openhands/dnsconfig.json
simplelogin MX 10800 10 mx1.simplelogin.co. infra/dns/dnsconfig.json
simplelogin MX 10800 20 mx2.simplelogin.co. infra/dns/dnsconfig.json
simplelogin TXT 10800 sl-verification=bxfzzfjiggzsxyzxhhmkmjqkaskjgy infra/dns/dnsconfig.json
simplelogin TXT 10800 v=spf1 include:simplelogin.co ~all infra/dns/dnsconfig.json
switch1.dc1 A default 192.168.1.254 infra/mikrotik/dnsconfig.json
t3code.host-bot.simeonwarren.users A default 192.168.1.210 users/simeonwarren/host_bot/dnsconfig.json
threexui A default 192.168.10.80 infra/threexui/dnsconfig.json
vault A default 192.168.1.218 infra/vault/dnsconfig.json
vault.dc1 A default 192.168.1.218 infra/vault/dnsconfig.json
www CNAME 300 alwaldend.com. infra/dns/dnsconfig.json
www-staging CNAME 300 alwaldend.github.io. infra/dns/dnsconfig.json
xcp-ng A default 192.168.1.213 infra/xcp_ng/dnsconfig.json
xoa.xcp-ng A default 192.168.1.206 infra/xcp_ng/dnsconfig.json
yandex MX 21600 10 mx.yandex.net. infra/dns/dnsconfig.json
yandex TXT 300 v=spf1 redirect=_spf.yandex.net infra/dns/dnsconfig.json
yandex TXT 300 yandex-verification: b83672f59b3dbe16 infra/dns/dnsconfig.json
yc.threexui NS default ns1.yandexcloud.net. infra/threexui/dnsconfig.json
yc.threexui NS default ns2.yandexcloud.net. infra/threexui/dnsconfig.json
yc1.nodes.threexui CNAME default host1.nodes.yc.threexui.alwaldend.com. infra/threexui/dnsconfig.json

4.3 - OpenSpec

4.3.1 - Infrastructure DNS

Describe owner-local Terraform DNS declarations and runtime source-ownership validation for global and dc1 records. This contract describes checked-in source; it does not establish live adoption or authorize infrastructure writes.

Sources: component documentation, target definitions, runtime linter, and Terraform root.

DNS declarations SHALL remain in their owners’ dnsconfig.json files. Runtime inventory SHALL discover all canonical files, including nested modules and empty declarations, without a checked-in ownership registry. The inventory SHALL print a deterministic table of files, DNS names, types and views.

Project landing declarations retain their declared direct targets.

  • WHEN the runtime linter scans a workspace containing the new file
  • THEN it includes the file without requiring a registry update

The Terraform module SHALL preserve Cloudflare global and MikroTik dc1 views. It SHALL retain every type member, expand and deduplicate destinations, reject unknown inputs and preserve effective values, TTLs, priorities and multiplicity.

Scenario: An entry declares A and AAAA in all views

  • WHEN the shared module normalizes the entry
  • THEN both types appear exactly once in each destination view
  • WHEN a record names an unsupported destination
  • THEN normalization fails with an actionable diagnostic

The linter and module tests SHALL validate source ownership, names, inputs, normalization and provider mappings without credentials or live provider access. Existing JSON-based VM consumers SHALL remain compatible.

  • WHEN offline checks run
  • THEN they validate the declaration and fixtures without deploying records

Owner Terraform roots SHALL use the existing AL/Vault authentication, backend and credential injection workflow. They SHALL obtain only their required view credentials. Operational provider initialization may require live credentials even with resource creation disabled. Central DNSControl write entrypoints SHALL be removed from the candidate.

  • WHEN an authorized operator invokes the root after bootstrap
  • THEN it uses that owner’s AppRole and Cloudflare credentials
  • AND RouterOS credentials and resources are not required

Each owner SHALL instantiate the shared module in tf_setup when present, otherwise in tf. Missing roots and AppRoles SHALL be added. Related AppRole resources SHALL be grouped in modules under infra/vault/tf/approles/<name>. Provider instance keys SHALL be stable across value-only changes; unrelated owners SHALL remain outside the state.

  • WHEN the integration is prepared
  • THEN only its setup root declares the DNS module

The runtime linter SHALL reject different files managing the same canonical fully qualified DNS name, across types and views. Multiple records within one file SHALL be allowed. Diagnostics SHALL identify both files.

  • WHEN one file declares an A record and another an AAAA record or a different view for the same name
  • THEN lint fails and identifies both source files
  • WHEN one file declares several values, types or views for a name
  • THEN ownership lint accepts the sole source

Operational cutover SHALL audit active and scheduled central writers, stop identified competing writers, and coordinate one owner at a time. Evidence SHALL state the audit’s coverage and unavailable observations. Each existing record SHALL be imported using its actual provider ID into exactly one owner state, followed by a no-change adoption plan before writes. Fresh complete inventories MAY establish missing declarations for an exact additions-only plan that preserves every existing record. Endpoint-owning batches SHALL verify an independent authenticated recovery path first.

The shared apex and mail records owned by infra/dns SHALL remain disabled by default in its tf root before the authorized adoption revision and SHALL retain enabled ownership after import. Reconciliation against the adopted state and unchanged declarations SHALL propose no record additions, changes, replacements, or deletions and SHALL preserve unrelated provider records.

  • WHEN its authorized cutover runs
  • THEN exact-ID imports preserve existing records
  • AND the operator coordinates exclusive active writers and verifies unrelated records remain unchanged
  • WHEN complete provider inventories establish that declared names are absent and conflict-free
  • THEN the reviewed DNS plan creates only those missing declarations
  • AND every existing provider identity and attribute remains unchanged
  • AND a follow-up DNS plan contains no changes
  • WHEN the shared tf root uses its checked-in defaults before its authorized adoption revision
  • THEN shared-record ownership is disabled and the root retains the canonical shared declaration and module inputs
  • WHEN the adopted shared tf root uses its checked-in source defaults
  • THEN ownership is enabled for the shared apex and mail records in their declared views
  • WHEN the shared tf root plans against adopted state and unchanged declarations
  • THEN it proposes no record additions, changes, replacements, or deletions
  • AND unrelated provider records remain unchanged

The implementation SHALL derive its declaration views from the owning dnsconfig.json files rather than maintaining a normalized inventory or BIND exporter. Historical provider snapshots SHALL NOT be presented as current desired state, and no provider snapshot SHALL be committed as a declaration source.

  • WHEN the runtime linter succeeds
  • THEN its table reports the discovered declarations without an export step

The implementation SHALL render one documentation page per destination view from the declared records, with the owning declaration for every record. Regeneration SHALL require an explicit manual command. Ordinary validation SHALL check declarations and ownership without requiring checked-in pages to match the declarations or rewriting those pages. Documentation SHALL identify the declarations as authoritative and explain that snapshots may lag.

  • WHEN an operator explicitly runs the documentation generation command
  • THEN each destination page lists the current declarations with their owner
  • AND an explicit freshness check succeeds against the regenerated pages
  • WHEN a declaration changes while its checked-in page remains unchanged
  • THEN ordinary declaration validation does not fail because of that stale page
  • AND the page is not rewritten unless regeneration is explicitly invoked
  • WHEN an operator explicitly checks a stale page
  • THEN the command reports that regeneration is needed

4.4 - Shared DNS Terraform

Owner state for common apex and mail records

This root consumes the canonical common declarations through the reusable DNS module. It uses the src_infra_dns AppRole and its existing Vault-backed HTTP state. Both Cloudflare and RouterOS credentials are injected through the tf=1 stage label.

Shared apex and mail records were adopted on 2026-09-13. dns_enabled now defaults to true; reconciliation against the adopted state and unchanged declarations must propose no record changes. The adoption evidence records 60 imports and preservation of both complete provider inventories.

Operational calls require the AppRole policy and Vault credential fields described by the cutover procedure. The module’s import_addresses output gives addresses relative to module.dns; actual provider IDs come from the authorized adoption inventory.

Build the wrappers or run :tf_tests.fmt_test for offline validation. Import, plan and apply follow the separately authorized adoption procedure.

5 - Download

Public static files and websites on XCP-ng and Yandex Cloud

Static hosting architecture

One VM per environment runs the shared Traefik service in front of native Nginx. Clients connect directly on ports 80/443; Nginx listens only on 127.0.0.1:8008. Public DNS selects Yandex and dc1 DNS selects XCP-ng. These are independent stores with no replication, failover, or backups.

Each VM has a 100 GiB Btrfs content disk mounted at /srv/download:

projects/<project>/releases/<version>/<uploaded files>
sites/alwaldend.com/releases/<version>/<extracted website>
sites/alwaldend.com/current -> releases/<version>
staging/
state/

download.alwaldend.com/projects/ returns JSON directory listings and serves file bytes, including website archives. alwaldend.com serves the selected extracted website. www.alwaldend.com redirects permanently to the apex, preserving the path and query. Staging and service state have no public route. Directory listings never use a release’s index.html as an index page.

The download account owns content and staging; Ansible does not configure its authorized_keys. Administrators use existing SSH access and sudo to run publication as this account. The host role installs rsync. Nginx reads content and cannot write it. Ansible never publishes or selects a release.

The release tool and component publishing targets are delivered in PR #113, independently against master. Their commands and transfer behavior belong to the release tool documentation. The website browser change also remains separate implementation work.

A daily download-deduplicate.timer runs duperemove over projects/ and sites/ on each VM. It shares identical extents while retaining independent files, paths, permissions, and copy-on-write behavior. Repeated website assets can share storage; separately compressed archives may have fewer identical extents. Savings are workload-dependent and stay within each environment.

A private hash database under state/deduplication/ makes subsequent runs incremental. Staging, ACME state, and the database are outside the scan roots. The job runs with one I/O and one CPU thread, idle I/O priority, a half-core CPU limit, and a 512 MiB memory limit. Systemd serializes runs; the timer makes up a missed run after downtime. The service requires the content mount.

The job never deletes or activates releases. Its result is available through systemctl status download-deduplicate.service and the service journal. btrfs filesystem du reports shared/exclusive allocation; ordinary file sizes and download responses remain their logical sizes. The declared filesystem setup does not force formatting over an existing filesystem.

Terraform provisioning, provider assignments, and DNS declarations are merged through PR #108. Host configuration connects by unique inventory FQDNs and needs no packaged DNS inputs. The shared AL configuration selects provisioning or host credentials by stage. DNS snapshots are regenerated only on manual request.

local, yandex, and dns are separate Terraform roots directly under this project. Their separate state keys are alwaldend.com/vault1/approles/src_infra_download/tf_backend/<stage>. Selecting one root cannot plan deletion of another root’s resources, and no root reads another root’s complete Terraform state.

  • local owns the XCP-ng VM and two disks. prevent_destroy blocks VM replacement because the pinned provider cannot retain and reattach its data disk separately. Replacement needs a future reviewed storage workflow.
  • yandex owns the cloud VM, network, firewall, reserved public address, retained content disk, and its Yandex DNS zone and host record. VM replacement reuses the content disk and reserved address.
  • dns owns only this component’s Cloudflare/RouterOS aliases and delegation. Each stage has its own Vault HTTP state key.
  • infra/dns retains apex, mail, and www ownership. Its public apex follows download.alwaldend.com; that stable service name follows the component’s Yandex host. Its local apex A record is the canonical VM address consumed by Terraform. The unique local host CNAME and download alias follow that local apex. Endpoint changes therefore have one maintained input per environment. Ansible connects by inventory FQDN, without parsing DNS source. Old Pages A/AAAA records are removed through staged owner cutovers; unrelated mail and staging records remain.
  • ansible mounts storage, configures SSH and service access, labels static content for SELinux, and deploys the shared Traefik and Nginx roles.

The local address 192.168.10.66 is unclaimed in checked-in DNS declarations; confirm its availability and the named XCP-ng template/resource-set inventory before deployment. The cloud image defaults to the immutable Fedora 43 image already used by infra/threexui; local provisioning uses Fedora 44.

The merged infra/vault/approles/src_infra_download root owns the component identity. Its applied identity and core memberships are documented by the Vault owner. infra/xcp_ng/tf owns the XO resource-set assignment; infra/yandex_cloud/org1/tf owns the folder and its standard folder-scoped admin service account. AL selects only the chosen stage’s provider credentials and state backend.

AL reads the existing Cloudflare/RouterOS credentials for DNS provisioning, and the component Yandex account from Vault. Traefik uses HTTP-01 in both environments: Let’s Encrypt on Yandex, and the existing Vault ACME service with role-scoped EAB on XCP-ng. The component’s Vault PKI role permits only the apex, download, and www names. Local clients must trust the repository CA; neither environment requires a client certificate or login. Traefik configuration and private ACME state stay on the system disk under /opt/traefik, independent of the content mount. DNS provider credentials are not installed on either host.

The shared host role installs the CA needed to reach Vault. Each inventory key is the unique host FQDN used for connection and SSH host-key checks. The shared SSH role includes inventory_hostname when signing keys; no host-key alias or address override is needed.

The following are operator entry points, not evidence of a deployed service. Confirm the local address, named XO inventory, template disk/interface layout, and cloud image before deployment. Each live operation requires its own approved scope.

  1. Complete the Vault prerequisite and ensure the existing XO OIDC service is configured through its owner. Obtain explicit authorization for the first-login bootstrap of src_infra_download: it creates the external XO identity/groups. Run the existing bootstrap target and record its result:

    XO_BOOTSTRAP_APPROLE=src_infra_download bazel_agent bazel run //infra/xcp_ng/cmd/xo_login:bootstrap
    
  2. After that login, review/apply the XO resource-set assignment through infra/xcp_ng/tf to reconcile the identity’s bindings in IaC. Confirm the AppRole is absent from approles_pending_oidc_login and its resource set has the intended subject before planning the local VM. Review/apply the Yandex folder assignment through infra/yandex_cloud/org1/tf as well.

  3. Review and apply //infra/download/local:tf.plan/tf.apply and //infra/download/yandex:tf.plan/tf.apply independently.

  4. Prepare the dc1 apex A record through infra/dns/tf while preserving public Pages records, following the local DNS procedure. Apply //infra/download/dns:tf.plan/tf.apply to establish cloud delegation and host/service aliases. Verify both inventory FQDNs before Ansible. The local website is in maintenance until host configuration and publication.

  5. Run //infra/download/ansible:ansible.local or :ansible.yandex. There is deliberately no unqualified command that configures both environments. Bootstrap host-key trust through the existing SSH procedure before the first connection; subsequent host certificates use the component role.

  6. Publish the site content and verify HTTP routing with address overrides. Follow the two-phase apex cutover through //infra/dns/tf:tf.plan/tf.apply. HTTP-01 requires public DNS/port 80 to reach Yandex and Vault’s DNS view/port 80 to reach XCP-ng before either issuer can validate the three names.

  7. Exercise Let’s Encrypt staging with an explicit directory override and separate ACME storage, then select production; test Vault issuance locally. This sequence has a certificate-bootstrap interval after DNS cutover. Verify both trust chains, renewal, www redirects, downloads, and reboot persistence. Keep the previous deployment available for a reviewed recovery.

Use bazel_agent bazel run <target> for these commands. Provider credentials and backend state are injected by AL; no host-installed Terraform or Ansible is required. See the acceptance matrix for isolated checks and the remaining live acceptance cases. Provisioning evidence retains the Terraform validation and review history.

5.1 - Download DNS

Cloudflare and RouterOS aliases for static hosting

Owns the component’s dnsconfig.json through the shared DNS module. Public download requests follow the delegated Yandex host. The local host CNAME follows the apex owner’s canonical local A record. The apex and www keep their existing owner; DNS cutover is a separately reviewed live operation.

This root selects only DNS credential injection and its own state backend. It does not authenticate with either VM provider or require either VM’s state.

Before configuring the local host by its inventory FQDN, prepare and commit a reviewed intermediate infra/dns/dnsconfig.json revision that installs the final download_dc1 A record in the dc1 view and removes the old dc1 Pages A/AAAA members. Keep the old public Pages A/AAAA records and all unrelated records unchanged; split prior dsp: ["all"] declarations by view as needed. Plan and apply that exact revision through infra/dns/tf, after separately authorizing this local DNS change. Review that only the intended dc1 apex records change and verify the local answer before continuing.

Apply this component’s aliases after that preparation, then confirm host1.dc1.download.alwaldend.com resolves to the address used by the VM. Ansible connects using that unique hostname. The local website is in a maintenance interval until host configuration and content publication finish; the public website continues using Pages during this local preparation.

The local VM consumes the same apex A value from source. The host CNAME and local download alias follow it, so renumbering has one maintained address. The dc1 apex also declares the unusable AAAA address ::ffff, following RouterOS’s documented override. An A record alone does not suppress upstream AAAA answers: while public DNS still points to Pages, IPv6 clients would reach that site instead of the local VM. This local override keeps clients on the configured IPv4 endpoint without changing public DNS or relying on a guest’s temporary IPv6 address. The public apex follows download.alwaldend.com, which in turn follows the component-owned Yandex host. Renaming that host therefore requires no copied endpoint edit in the apex owner.

The apex belongs to infra/dns/tf, separately from this component DNS root. Its old GitHub Pages A/AAAA records and the new public CNAME have different Terraform instance keys. Applying the final declaration directly can race CNAME creation against address-record deletion. Cloudflare rejects that coexistence; use two sequential, reviewed IaC revisions and saved plans.

Before either phase, complete the DNS owner’s adoption procedure, verify both hosts and content using address overrides, configure certificate issuers, and authorize the apex cutover explicitly. Record the prior source revision and recovery plan. Schedule a maintenance window: the public apex has an intentional address gap between the phases, and resolver caches can extend that interval.

  1. Prepare and commit a reviewed intermediate infra/dns/dnsconfig.json revision with the old public apex A/AAAA members removed from the global view and records.download_global absent. Keep local records, www, mail, TXT, and unrelated records at their prior values. Through //infra/dns/tf:tf.plan, save and review a plan that deletes only the old public apex A/AAAA instances and creates no apex CNAME. Apply that exact saved plan through //infra/dns/tf:tf.apply. Verify completion and the authoritative Cloudflare inventory shows those A/AAAA records are gone.
  2. Only after phase 1 succeeds, select the reviewed final declaration from this change, including records.download_global. Create a fresh saved plan through the same owner; review the CNAME creation and intended local apex changes while preserving mail/TXT/www and unrelated records. Apply that plan, then verify authoritative records and both DNS views, website responses, and redirects.

Keep all source/input files unchanged between each saved plan and its apply. If a phase fails, stop and inspect the actual record/state inventory before planning recovery. Restore the recorded prior declaration only through a reviewed owning-root plan; remove any conflicting CNAME first if returning to the old A/AAAA records. Never begin phase 2 based only on a planned deletion.

5.2 - Host configuration

Shared native Traefik and Nginx deployment

Build :ansible_bin to package the playbook and shared collection. The play uses alwaldend.main.download_host for storage, the publisher account, rsync, service setup, and deduplication. The role does not configure publisher authorized keys; administrators use their existing SSH access and run publication through sudo as download. Inventory, Vault injection, and routing templates belong to this deployment. Both inventories connect using their unique host FQDNs, with the same names for SSH host-key checks. Host DNS must resolve before configuration; no IP override or HostKeyAlias is configured. Run :ansible.local or :ansible.yandex only with explicit live authorization. Before configuring the host, Ansible requires the content path to resolve to a block device. The role creates the filesystem without forcing an overwrite and mounts it before enabling the services. The shared roles manage the service units. Traefik keeps its configuration and ACME data on the system disk at /opt/traefik and has no content-mount dependency. The role creates publication roots; publishers create individual project/site directories and select releases. It never uploads or selects releases.

nginx_config_template supplies the complete configuration to the reusable Nginx role. JSON listings allow credential-free cross-origin reads and normal HEAD/range downloads. Site requests follow the current link. Port 8008 is loopback-only and uses the Fedora SELinux HTTP port type. Public content has read-only HTTP labels; the SSH publisher needs readable file modes and must preserve those labels when activating staged content.

Yandex uses Let’s Encrypt HTTP-01 without EAB; XCP-ng uses the existing Vault HTTP-01 directory with EAB generated by the shared Traefik role. Local browsers need the repository CA. Neither host stores a DNS provider credential.

For public ACME staging, override both download_acme_directory and traefik_data_dir (for example a separate acme-staging directory under the Traefik root). Switching back preserves separate staging/production accounts and prevents staging certificates from becoming the production selection.

The content filesystem is Btrfs. The native duperemove package and daily systemd timer share duplicate extents across published files and extracted sites. Its private hash database lives outside both scan roots. The maintenance service waits for the mount and has bounded CPU/memory and low I/O priority; a failed run leaves the release layout and active website selection intact.

The play forces notified handlers after later task failures. Successfully installed configuration therefore still triggers its pending handlers if a later task fails. Unreachable hosts can still prevent handler execution. Nginx configuration validation before installation is unchanged. See Ansible’s handler failure behavior.

5.3 - XCP-ng

Protected local static-hosting VM

Provisions one Fedora 44 VM in src_infra_download, using named inventory from the existing XCP-ng deployment: two CPUs, 2 GiB RAM, a 20 GiB boot disk, and a 100 GiB content disk. The local address comes from the apex DNS owner, whose local A record is also the target of the unique host CNAME. The cloud template must have one boot disk and the expected enX0 interface; Ansible formats and mounts the second disk at /srv/download without force.

prevent_destroy rejects replacements and destruction because the pinned XO provider owns disks with the VM. There is no automatic retained-disk replacement workflow on XCP-ng. Do not remove that guard as a routine upgrade step.

tf.plan and tf.apply are live operations. tf_tests.fmt_test and the offline entry point with backend-disabled initialization validate source.

5.4 - Yandex Cloud

Static-hosting VM with retained local content storage

Provisions one Fedora VM in its component folder: two CPUs, 2 GiB RAM, a 20 GiB boot disk, and a separate 100 GiB content disk. The disk and reserved IPv4 address have destruction guards and survive replacement of the VM. The instance firewall permits TCP 22/80/443; Nginx has no public listener.

The delegated Yandex DNS zone’s A record follows the reserved address. Cloudflare delegation and service aliases belong to the sibling DNS root. AL injects the folder ID and folder-scoped service account; no credentials or allocated addresses are copied into checked-in variables.

6 - flux

Fluxcd deployment

This diagram describes the maintained architecture source, not live inventory or service health. The Mermaid source is authoritative; Bazel renders its light and dark SVGs.

Flux architectureFlux architecture

ssh -L 6443:127.0.0.1:6443 -N flux.alwaldend.com
bazel run //infra/flux/cl:flux
bazel run infra/flux/cl:flux -- get all -A
bazel run //infra/flux/cl:flux.bootstrap
bazel run //infra/flux/cl:op
bazel run //infra/flux/cl:oidc
bazel run //infra/flux/cl:cmctl -- renew traefik-gateway-websecure-tls -n traefik
  • Generate secret id and the token:
    SECRET_ID=$(bazel run //infra/flux/cl:vault.secret_id | jq -r .data.secret_id)
    TOKEN=$(echo "${SECRET_ID}" | bazel run //infra/flux/cl:vault.ops_token | jq -r .auth.client_token)
    echo "Secret id: ${SECRET_ID}, Token: ${TOKEN}"
    
  • Patch secret-id in ./cl/cert-manager/issuer-approle.yaml
  • Patch token in projects/kustomization/flux-repo/flux-sops-secret.yaml
  • Encrypt:
    bazel run //infra/flux/cl:sops.encrypt infra/flux/cl/cert-manager/issuer-approle.yaml
    bazel run //infra/flux/cl:sops.encrypt projects/kustomization/flux-repo/flux-sops-secret.yaml
    bazel run //infra/flux/cl:sops.encrypt projects/kustomization/flux-repo/flux-git-src-secret.yaml
    

6.1 - Ansible

Ansible setup

6.2 - cl

Cluster manifests

6.3 - OpenSpec

6.3.1 - Specifications

6.3.1.1 - Infra flux

6.3.1.1.1 - Flux infrastructure specification

Describe the Flux management cluster’s VM definition, K3s configuration, and Git reconciliation entry points owned by infra/flux. The tf_setup, ansible, and cl packages implement stages of this owner. This is a checked-in source baseline; no cluster health or successful deployment was observed for this specification.

Baseline revision: 550d7e79b1f5fdbc2b6017b75178471d6914082f. Observed: 2026-09-08. Sources: owner README, BUILD, and the implementation links below.

Terraform setup SHALL declare a Proxmox VM in the src_infra_flux pool with two cores, 4096 MiB memory, and separate 20 GiB boot, K3s data, and K3s storage disks. The deployment playbook SHALL apply the shared host and K3s roles, and the K3s configuration SHALL enable secrets encryption and disable the bundled Traefik component.

Sources: VM definition, deployment playbook, and K3s configuration.

  • WHEN a maintainer evaluates the setup and deployment source
  • THEN VM provisioning, host configuration, and K3s configuration are separate stages with the recorded compute and disk allocation
  • AND the K3s configuration enables secrets encryption at rest

The cluster’s root Kustomization SHALL include the Flux and Harbor reconciliation trees. Flux’s reconciliation resources SHALL read the flux-git-src GitRepository and use SOPS decryption through flux-sops-secret, with pruning enabled and forced replacement disabled. The Harbor tree SHALL include its remote-cluster configuration and Harbor’s Flux manifests.

Sources: root Kustomization, Flux reconciliation, and Harbor reconciliation tree.

  • WHEN the checked-in root Kustomization is traversed
  • THEN both src-infra-flux-cl and src-infra-harbor-cl are included
  • AND the Flux reconciliation resources retain their Git source, decryption reference, and explicit prune and force settings

The cluster package SHALL provide repository-configured Flux, Flux operator, kubectl, Helm, certificate-manager, and SOPS command wrappers. Its Flux instance SHALL declare source, Kustomize, Helm, notification, image-reflector, image-automation, and source-watcher controllers, with network policy enabled.

Sources: cluster BUILD and Flux instance.

Scenario: Select a repository operator entry point

  • WHEN an operator selects a declared cluster command target
  • THEN the target supplies the packaged tool and the Flux owner’s al configuration
  • AND the controller source records the enabled components without asserting that they are currently available in a live cluster

6.3.1.2 - Owned dns

6.3.1.2.1 - owned-dns Specification

Define Flux DNS management through the owner’s tf_setup root, including canonical declarations, scoped credentials, and offline source checks before adopting live records.

The tf_setup root SHALL consume this owner’s canonical dnsconfig.json through the shared DNS Terraform module, preserving declared record identities and views. DNS resources SHALL default to disabled before adoption and SHALL remain enabled by default after authorized adoption. Adoption SHALL bind existing provider records to the owner’s state without adding, changing, replacing, or deleting DNS records. Subsequent reconciliation of unchanged declarations SHALL preserve owned and unrelated records.

  • WHEN the checked-in root is evaluated with default inputs before adoption
  • THEN it reads this owner’s declaration and disables managed DNS records
  • AND the package contains the module and declaration inputs
  • WHEN the adopted root is evaluated with default inputs
  • THEN it reads this owner’s declaration and enables managed DNS records
  • AND the package contains the module and declaration inputs
  • WHEN the owner applies a reviewed saved import plan through //infra/flux/tf_setup:dns.apply
  • THEN the owner state binds each declared record to its existing provider identity
  • AND the reviewed adoption plan proposes no record additions, changes, replacements, or deletions
  • WHEN the adopted root plans and applies unchanged declarations through its //infra/flux/tf_setup wrappers
  • THEN it makes no DNS resource changes
  • AND owned records retain their declared values and views while unrelated records remain unchanged

The DNS wrappers SHALL select src_infra_flux through the repository AL flow and keep secret values in injected variables. The dns.plan, dns.show, and dns.apply targets SHALL select dns=1 and retain the owner’s setup backend; plans SHALL target module.dns and apply SHALL require a reviewed saved plan. Ordinary setup wrappers SHALL retain their service authentication behavior. Real DNS credentials and Vault policy grants SHALL be prerequisites for operational Terraform calls. RouterOS DNS credentials SHALL remain isolated from unrelated RouterOS resources. The package SHALL expose a format test without live credentials. DNS-targeted validation SHALL NOT establish unrelated VM or service health.

  • WHEN the package format test executes
  • THEN it checks the packaged Terraform configuration without live credentials
  • AND existing non-DNS authentication and backend paths remain unchanged
  • WHEN an authorized operator selects the owner’s DNS wrapper
  • THEN it selects DNS credential injection through the owner AppRole and setup backend
  • AND its scoped plan and saved-plan apply cover DNS and its dependencies
  • AND disabled DNS resources do not imply offline provider configuration

6.4 - Tf setup

Terraform setup

This root owns the Flux DNS declaration. DNS ownership defaults to enabled after verified adoption; keep it enabled to retain the managed records.

Use //infra/flux/tf_setup:dns.plan, :dns.show, and :dns.apply for scoped DNS reconciliation. These targets retain the setup backend and src_infra_flux AppRole, select dns=1, and target module.dns. Apply requires a reviewed saved plan. Reconciliation of unchanged declarations must produce no DNS changes.

The ordinary setup targets retain their service authentication behavior. DNS checks do not establish PVE VM or service health; current aggregate source validation remains part of repository delivery.

7 - forgejo

git.alwaldend.com

This diagram preserves the former Proxmox deployment. Forgejo now runs on XCP-ng; see Deployment for the maintained deployment contract. The Mermaid source is authoritative; Bazel renders its light and dark SVGs.

Historical Forgejo architectureHistorical Forgejo architecture

Service Terraform consumes the shared repository catalog for organization-owned repositories and named administrator/developer roles. Vault continues to own OIDC login identities and service-specific access. See service Terraform for identity validation, state adoption, and the preserved automation grants.

Forgejo is recreated on XCP-ng through Xen Orchestra, using the src_infra_dc1_forgejo1 resource set provisioned by infra/xcp_ng. VM provisioning authenticates to XO through Vault OIDC as Forgejo’s own AppRole. Its exact synchronized user receives the resource-set membership and an explicit administration ACL on the existing Forgejo VM; it does not use the shared infrastructure administrator token. These bindings must be applied by infra/xcp_ng/tf before Forgejo’s setup Terraform runs.

The canonical service URL is https://git.alwaldend.com. The previous forgejo.alwaldend.com name remains available for existing clients. The VM retains 192.168.10.40 and host1.forgejo.alwaldend.com. Its boot, Forgejo, and Traefik disks are 20, 40, and 5 GiB respectively.

See Terraform setup for the template, network, storage, and previous-state prerequisites. These commands create and configure a fresh service; they do not restore the destroyed Proxmox VM’s data.

Fresh host hardening can outlast the ten-minute Vault token. Run host setup and service deployment separately to obtain fresh authentication for each phase. Ansible creates the missing Vault OIDC login source after starting Forgejo; existing active Vault sources are preserved.

Before configuring Forgejo with Terraform, set TF_VAR_vault_oauth_source_id to the verified ID of the active vault OIDC source, as described in service Terraform. Its narrowly scoped Vault entity-read policy must be provisioned first so the required external users can be created.

The guest disables IPv4 redirect acceptance so its traffic uses the configured gateway. LAN clients must also retain symmetric routes: same-interface router redirects can otherwise make connections fail. Router redirect policy and stale client route caches must be resolved before relying on LAN access.

bazel_agent bazel run //infra/forgejo/tf_setup:tf_setup.apply
bazel_agent bazel run //infra/forgejo/ansible:ansible -- --skip-tags traefik,forgejo,forgejo_oidc
bazel_agent bazel run //infra/forgejo/ansible:ansible -- --tags traefik,forgejo,forgejo_oidc
bazel_agent bazel run //infra/forgejo/tf:tf.apply

7.1 - Ansible

Ansible setup

7.2 - OpenSpec

7.2.1 - Specifications

7.2.1.1 - Infra forgejo

7.2.1.1.1 - Forgejo infrastructure specification

Describe the Forgejo service owned by infra/forgejo, including Xen Orchestra provisioning, Ansible service configuration, and Terraform account and repository management. The tf_setup, ansible, and tf packages are implementation stages of this owner. This baseline records source guarantees and prerequisites; it does not verify live service health or restore earlier Forgejo data.

Baseline revision: 550d7e79b1f5fdbc2b6017b75178471d6914082f. Observed: 2026-09-08. Sources: owner README, BUILD, and the implementation links below.

Setup SHALL provision Forgejo through Xen Orchestra in the src_infra_dc1_forgejo1 resource set using the owner’s Vault AppRole and packaged XO OIDC login flow. The VM SHALL declare 20 GiB boot, 40 GiB Forgejo, and 5 GiB Traefik disks. Ansible SHALL verify the expected sizes of xvdb and xvdc before applying the service roles that use them.

Sources: setup contract, setup BUILD, VM definition, and deployment playbook.

  • WHEN Ansible gathers disks that do not match the expected Forgejo and Traefik device names and sizes
  • THEN its pre-task assertion fails before the service roles run

Forgejo SHALL configure https://git.alwaldend.com/ as its canonical root URL, bind its HTTP service to loopback port 3000, and enable the built-in SSH service on port 3005. Traefik SHALL route the canonical hostname, the inventory hostname, and forgejo.alwaldend.com to that HTTP service with the Vault certificate resolver.

Sources: service variables, Forgejo configuration, and Traefik routing.

  • WHEN the Traefik routing template is rendered with the owner’s variables
  • THEN the canonical and legacy hostnames resolve to the same configured loopback service
  • AND generated Forgejo URLs use the canonical Git hostname

The deployment playbook SHALL create a missing vault OIDC authentication source after configuring the Forgejo service. It SHALL preserve one existing active OAuth2 source and reject multiple matches, an inactive source, or a source of another type. Creation SHALL obtain client credentials from the injected environment and suppress secret-bearing task output.

Source: deployment playbook.

  • WHEN Forgejo reports exactly one active OAuth2 source named vault
  • THEN the bootstrap accepts it and skips source creation
  • WHEN more than one vault source is discovered, or its type or active state is incompatible
  • THEN the bootstrap assertion fails rather than selecting or replacing an arbitrary source

Service Terraform SHALL require a verified positive integer Vault OAuth source ID, discover login users from the owning Vault group through at most two nested group levels, and use entity UUIDs as external login names. Managed accounts SHALL be protected from deletion. The shared repository catalog SHALL own named organization administrator and developer assignments and organization-owned repository identities. Catalog members and Vault access-group members SHALL belong to the discovered login population. Existing Vault service-administrator, package-writer, and automation-writer grants SHALL be retained; the src automation writer group SHALL resolve to exactly one user. A catalog developer SHALL NOT also receive administrator access through the retained Vault groups.

Sources: service Terraform contract, users, access validation, and repository access. The shared catalog contract owns repository naming and named-role assignments; its adoption change records the source migration and pending verification.

  • WHEN a login group extends beyond two nested levels, an access-group member is outside the login population, or the automation writer count is not one
  • THEN the corresponding Terraform condition rejects the configuration
  • AND Terraform does not silently omit the unsupported membership
  • WHEN a catalog administrator or developer is assigned organization access
  • THEN that assignment uses the discovered account’s existing Vault entity UUID and verified OAuth source mapping
  • AND the assignment does not create a password-based replacement account
  • WHEN shared named roles are adopted
  • THEN existing Vault service administration, package-writing, and automation-writer grants remain configured
  • AND the catalog developer receives feature-branch and pull-request access without a default-branch push or merge bypass
  • AND an overlapping administrator grant for that developer fails validation

7.2.1.2 - Owned dns

7.2.1.2.1 - owned-dns Specification

Define Forgejo DNS management through the owner’s tf_setup root, including canonical declarations, scoped credentials, and offline source checks before adopting live records.

The tf_setup root SHALL consume this owner’s canonical dnsconfig.json through the shared DNS Terraform module, preserving declared record identities and views. DNS resources SHALL default to disabled before adoption and SHALL remain enabled by default after authorized adoption. Adoption SHALL bind existing provider records to the owner’s state without adding, changing, replacing, or deleting DNS records. Subsequent reconciliation of unchanged declarations SHALL preserve owned and unrelated records.

  • WHEN the checked-in root is evaluated with default inputs before adoption
  • THEN it reads this owner’s declaration and disables managed DNS records
  • AND the package contains the module and declaration inputs
  • WHEN the adopted root is evaluated with default inputs
  • THEN it reads this owner’s declaration and enables managed DNS records
  • AND the package contains the module and declaration inputs
  • WHEN the owner reviews the matched existing provider imports through //infra/forgejo/tf_setup:dns.plan and :dns.show, then applies that saved plan through :dns.apply
  • THEN the owner state binds each declared record to its existing provider identity
  • AND the reviewed adoption plan proposes no record additions, changes, replacements, or deletions
  • WHEN the adopted root plans unchanged declarations through //infra/forgejo/tf_setup:dns.plan and applies its reviewed saved plan through :dns.apply
  • THEN it makes no DNS resource changes
  • AND owned records retain their declared values and views while unrelated records remain unchanged

The DNS wrapper SHALL select src_infra_dc1_forgejo1 through the repository AL flow and keep secret values in injected variables. Dedicated DNS commands SHALL select dns=1, retain the existing setup backend, and target module.dns without starting unrelated service authentication. DNS apply SHALL consume only a reviewed saved plan. Real DNS credentials and Vault policy grants SHALL be prerequisites for operational Terraform calls. RouterOS DNS credentials SHALL be isolated from unrelated RouterOS resources. The package SHALL expose a format test that does not authenticate to Vault or contact DNS providers.

  • WHEN the package format test executes
  • THEN it checks the packaged Terraform configuration without live credentials
  • AND existing non-DNS authentication and backend paths remain unchanged
  • WHEN the wrapper selects its Terraform stage labels
  • THEN it selects DNS credential injection through the owner AppRole with dns=1, the existing setup backend, and the module.dns target
  • AND disabled DNS resources do not imply offline provider configuration

7.3 - Tf

Terraform config

This package configures Forgejo organizations, repositories, and access from the shared repository catalog. The catalog owns named administrators, developers, repository identities, and destination names. Its Terraform module supplies the consumer’s repository projection. First-party names remain stable across forges; external fork and mirror names retain their original upstream identity.

Before applying, provision the Vault OIDC authentication source with Ansible and set TF_VAR_vault_oauth_source_id to its verified positive numeric ID. Obtain the ID from forgejo admin auth list on the instance using the service’s config and work path; confirm that the vault source is active and uses OpenID Connect with the expected Vault issuer. Do not assume source IDs survive instance recreation.

Terraform discovers users from the authoritative Vault src_infra_dc1_forgejo1_users login group, then looks up their entities, following the PVE Terraform pattern. Discovery includes direct members and two levels of nested groups, covering the current administrator membership. Deeper nesting fails validation rather than silently omitting accounts. Each account uses its Vault entity UUID as the OIDC login name and its canonical username/email metadata. No local password or user impersonation is used. The Forgejo AppRole receives the existing group/entity lookup permissions, as PVE does. Apply those Vault group memberships before running this package.

The accounts are protected from Terraform deletion. OIDC group claims retain ownership of administrator/restricted status after login. If a user already exists outside this state, verify its source and login-name mapping before importing it; never replace an existing account to resolve a name collision. Resource addresses remain keyed by Vault entity name, preserving existing managed users. The Forgejo service identity may already exist from the first OIDC login: verify and import it before applying its newly discovered resource.

Named organization administrator and developer assignments come from the catalog and must resolve to the discovered Vault login population. The Developer team can read repositories, write feature branches, and open pull requests. Its members receive no default-branch push or merge bypass.

Existing service administration remains sourced from src_infra_dc1_forgejo1_admins and its direct child groups. Those grants are retained for entities with a Vault AppRole alias, alongside catalog administrators. The separate instance-wide OIDC administrator claim remains owned by Vault. Validation rejects a catalog developer who also receives administrator access through those Vault groups. Package writers and the repository automation writer retain the grants from src_infra_dc1_forgejo1_package_writers and src_infra_dc1_forgejo1_src_writers. These dedicated groups avoid granting service permissions to AppRole credential issuers. The existing singleton collaborator still requires exactly one discovered writer. Access-group members outside the login population also fail validation.

Repository adoption preserves remote IDs. The existing singleton repository address has an explicit move to its catalog key, while account and service access addresses remain stable. Import an existing untracked destination before applying; never recreate it to resolve a name collision. Review the full plan before execution and obtain separate approval for any deletion or replacement. The retained branch rules continue to own administrator and automation-writer exceptions.

bazel_agent bazel run //infra/forgejo/tf:tf.plan
bazel_agent bazel run //infra/forgejo/tf:tf.apply

7.4 - Tf setup

Terraform setup

This package creates a Xen Orchestra VM in the src_infra_dc1_forgejo1 resource set. infra/forgejo/al.lua authenticates with Forgejo’s own Vault AppRole and the packaged XO OIDC login plugin. The plugin supplies a temporary XO token and revokes it on shutdown; no infrastructure administrator token is loaded. The setup HTTP backend remains owned by the same Forgejo config.

Before running this package, bootstrap the AppRole’s XO OIDC user and apply its resource-set membership and existing VM ACL through infra/xcp_ng/tf. Subjects are matched by immutable Vault entity UUID under the configured OIDC issuer, not by login name or AppRole group membership. See XO authentication.

Defaults select the imported Fedora 44 template, local storage and wired network by name from the Forgejo resource set. Override TF_VAR_xoa_template_name, TF_VAR_xoa_storage_name, and TF_VAR_xoa_network_name for another assignment. Native provider lookups run as Forgejo’s own identity and reject ambiguous matches. No infrastructure UUID defaults are required; resolved IDs are passed to the XO API. The template must contain a single boot disk no larger than 20 GiB, Fedora, and cloud-init. Bootstrap installs Xen guest tools. The pinned Fedora 44 image uses predictable interface naming; xoa_guest_interface defaults to enX0 for its first Xen interface and can be overridden for another template. The selected network must carry 192.168.10.0/24; gateway and DNS default to 192.168.10.1 (the wired router) and can be overridden with the Terraform variables.

The PVE-based infra/cloud_init:xen_linux target supplies the shared Ansible user and CA configuration through the repository template rule. Terraform adds the hostname and static network configuration from ../dnsconfig.json. After cloud-init completes, verify that /dev/xvda is the boot disk, /dev/xvdb is the 40 GiB Forgejo disk, and /dev/xvdc is the 5 GiB Traefik disk before running Ansible, which creates filesystems on the latter two devices.

The old Proxmox VM was destroyed and its obsolete Terraform state binding was removed without issuing a destroy. This package now uses only XO. There is no cross-provider state move or data restoration in this package.

bazel_agent bazel run //infra/forgejo/tf_setup:tf_setup.plan
bazel_agent bazel run //infra/forgejo/tf_setup:tf_setup.apply

Use this package’s dns.plan, dns.show, and dns.apply targets for the scoped DNS workflow. They select dns=1, retain the existing setup backend and src_infra_dc1_forgejo1 AppRole, and target module.dns without starting unrelated service authentication. Inspect the saved plan through dns.show and pass only that reviewed file to dns.apply.

dns_enabled defaults to true after verified adoption. Keep it enabled to retain existing records; disabling it would propose deletion. Follow the cutover procedure for prerequisites, reconciliation, and recovery. The owner adoption change records the historical import and DNS verification evidence; scoped DNS checks do not establish service or VM health.

8 - Forgejo runner

Forgejo Actions runner deployment
bazel run //infra/forgejo_runner/tf_setup # Create VMs
bazel run //infra/forgejo_runner/ansible # Configure VMs

8.1 - Ansible

Forgejo runner configuration

8.2 - OpenSpec

8.2.1 - Specifications

8.2.1.1 - Infra forgejo runner

8.2.1.1.1 - Forgejo runner infrastructure specification

Describe the Forgejo Actions runner VM and deployment scaffold owned by infra/forgejo_runner. Its tf_setup and ansible packages are stages of this owner. The runner role is commented out in the baseline playbook; the checked-in configuration therefore does not establish a deployed or registered Actions worker. No live runner state was observed.

Baseline revision: 550d7e79b1f5fdbc2b6017b75178471d6914082f. Observed: 2026-09-08. Sources: owner README, BUILD, and the implementation links below.

Setup SHALL declare runner1 as Proxmox VM 1200 in the src_infra_forgejo_runner pool, with eight cores, 16 GiB memory, a 20 GiB boot disk, and a 300 GiB runner disk on ceph-ec storage. Its hostname and address SHALL derive from the owner’s DNS declaration.

Source: VM definition.

  • WHEN a maintainer evaluates the runner VM configuration
  • THEN the source declares the dedicated runner pool, compute allocation, and separate boot and runner disks
  • AND DNS source supplies the hostname and address

The baseline deployment playbook SHALL apply the shared host role to the forgejo_runner inventory group. The specification MUST identify the commented-out Forgejo runner role as inactive and MUST NOT interpret the available runner variables as evidence that the worker is installed or registered.

Sources: deployment playbook and runner variables.

  • WHEN the current playbook’s active roles are inspected
  • THEN only the shared host role is enabled
  • AND runner installation and registration remain outside the behavior established by this source baseline

The owner SHALL expose Terraform setup and packaged Ansible entry points through its al configuration and Vault injection dependencies. The runner configuration SHALL reference https://git.alwaldend.com, /dev/sdb, and an environment-supplied FORGEJO_RUNNER_TOKEN rather than a checked-in registration token.

Sources: setup BUILD, Ansible BUILD, and runner variables.

  • WHEN a maintainer inspects the runner configuration scaffold
  • THEN its token value is an environment lookup and its instance URL is the canonical Forgejo service
  • AND the token reference alone does not enable the inactive runner role

8.2.1.2 - Owned dns

8.2.1.2.1 - owned-dns Specification

Define Forgejo Actions runner DNS management through the owner’s tf_setup root, including canonical declarations, scoped execution, adoption of existing records, and offline source checks.

The tf_setup root SHALL consume this owner’s canonical dnsconfig.json through the shared DNS Terraform module, preserving declared record identities and views. DNS resources SHALL default to disabled before adoption and SHALL remain enabled by default after authorized adoption. Adoption SHALL bind existing provider records to the owner’s state without adding, changing, replacing, or deleting DNS records. Subsequent reconciliation of unchanged declarations SHALL preserve owned and unrelated records.

  • WHEN the checked-in root is evaluated with default inputs before adoption
  • THEN it reads this owner’s declaration and disables managed DNS records
  • AND the package contains the module and declaration inputs
  • WHEN the adopted root is evaluated with default inputs
  • THEN it reads this owner’s declaration and enables managed DNS records
  • AND the package contains the module and declaration inputs
  • WHEN the owner adopts matched existing provider records through //infra/forgejo_runner/tf_setup:dns.plan, :dns.show, and :dns.apply
  • THEN the owner state binds each declared record to its existing provider identity
  • AND the reviewed adoption plan proposes no record additions, changes, replacements, or deletions
  • WHEN the adopted root plans and applies unchanged declarations through its scoped //infra/forgejo_runner/tf_setup:dns wrappers
  • THEN it makes no DNS resource changes
  • AND owned records retain their declared values and views while unrelated records remain unchanged

The DNS wrapper SHALL select src_infra_forgejo_runner through the repository AL flow and keep secret values in injected variables. Dedicated DNS plan, show, and apply targets SHALL select dns=1 alone and target module.dns while reusing the owning setup root and backend. DNS apply SHALL consume only a reviewed saved plan. Ordinary setup targets SHALL retain their existing tf=setup flow. Real DNS credentials and Vault policy grants SHALL be prerequisites for operational Terraform calls. RouterOS DNS credentials SHALL be isolated from unrelated RouterOS resources. The package SHALL expose a format test that does not authenticate to Vault or contact DNS providers.

  • WHEN the package format test executes
  • THEN it checks the packaged Terraform configuration without live credentials
  • AND existing non-DNS authentication and backend paths remain unchanged
  • WHEN a dedicated DNS target selects its execution label
  • THEN it selects dns=1 and DNS credential injection through the owner AppRole
  • AND it retains the setup backend while limiting reconciliation to module.dns
  • AND disabled DNS resources do not imply offline provider configuration

8.3 - Terraform setup

Forgejo runner VM

Use this package’s dns.plan, dns.show, and dns.apply targets for the scoped DNS adoption workflow.

These targets select dns=1 and target module.dns with the existing src_infra_forgejo_runner AppRole and setup backend. Plan declared imports, inspect the saved plan with dns.show, and apply only the reviewed file with dns.apply. Ordinary setup commands retain their tf=setup flow.

dns_enabled defaults to true after verified adoption. Keep it enabled to retain existing records; disabling it would propose deletion. The adoption record contains the import, follow-up plan, inventory, and DNS verification evidence. The shared cutover procedure owns prerequisites and recovery. Scoped DNS checks do not establish VM or runner service health.

9 - GitHub

GitHub organization, repositories, access, and Pages configuration

This project manages the GitHub organization and repositories declared by the shared repository catalog, including existing forks, project landing sites, organization membership, and developer access.

Service Terraform adopts existing resources and preserves their identities. It also manages repository default-branch rules and existing Pages configuration. Site content is published through the owning project deployment targets; this infrastructure package does not publish it.

9.1 - GitHub Terraform

GitHub organization, repository, access, and Pages management

This package consumes the shared repository catalog through its provider-free Terraform module. The catalog supplies the GitHub owner, repositories, default branches, repository settings, organization administrators, and developers. This root supports one GitHub organization per provider instance and rejects multiple configured owners.

Existing repositories are imported, including forks. The apex site repository keeps its Pages configuration and custom domain. Published repository names and Pages domains come directly from the catalog; they are not regenerated from the current build-project registry.

Dedicated per-project landing repositories were retired: the main site now publishes every project landing at /projects/<name>/, so the catalog carries no landing_project key and this root manages no per-project Pages repository, custom domain, github-pages environment, or landing default branch.

The checked-in import blocks adopt the organization, existing repositories, default branches, memberships, developer collaborators, and both existing src rulesets. A repository with an observed catalog github.id must retain that numeric identity. An import failure must be resolved before applying; never remove an import merely to create an existing object again. Repositories, memberships, access grants, environments, and rulesets have deletion guards. Review the complete plan and obtain explicit authorization for any deletion or replacement. Previously authorized retirement may proceed within its exact reviewed scope.

Organization settings own only catalog-defined repository base permissions. Billing, profile fields, and other organization defaults remain controlled by GitHub and are explicitly ignored. The required empty billing_email argument is an import-only placeholder: the existing billing address is retained from state and must never be committed or exposed in plan output. Do not create this organization-settings resource without its import block. Dependabot alert settings for newly adopted repositories are similarly preserved.

Catalog administrators are organization owners. Developers receive explicit write access on every catalog repository, while organization base permissions remain at the catalog setting. The grants are non-authoritative, preserving other collaborators. Each repository has an active default-branch ruleset restricting creation, updates, deletion, and force pushes, with an organization administrator bypass. Developers can push other branches and open pull requests, but cannot directly push or merge into the default branch. Forks also disable the upstream fetch-and-merge exception. Catalog repositories permit merge commits only; squash and rebase merges are disabled, so an accepted pull request records a merge commit whose parents include the reviewed feature commit. Both pre-existing src rulesets retain their separate release-branch protections and bypass actors; the default-branch ruleset also applies alongside them. Repository rulesets are available for these public repositories on GitHub Free.

The apex site repository receives its built Hugo site from //projects/alwaldend.com:deploy in the root workspace. Terraform manages the catalog-defined custom domain and Pages source. Keep all applies limited to reviewed, authorized repository and Pages changes. DNS is managed by the owning Terraform root; DNS documentation describes the workflow. Validate the public custom domain after rollout.

GitHub issues the custom-domain certificate as part of its Pages build. If a certificate must be reissued, remove the pages block from the apex catalog record in a reviewed plan, apply it, then restore it in a second plan; GitHub schedules a new certificate once the custom domain returns. Verify the pushed revision, Pages build state, DNS, and the served certificate before closing the recovery.

9.2 - OpenSpec

9.2.1 - Infrastructure GitHub

Describe Terraform ownership of the catalog-defined GitHub organization, repositories, member access, default-branch restrictions, and Pages settings. These requirements describe source guarantees; they do not assert that a plan has been applied or that published sites are healthy.

Sources: component documentation, Terraform workflow, shared catalog, packaged inputs, repository resources, organization access, branch rules, and provider configuration.

The package SHALL consume organization identity, administrator and developer lists, repository identity, default branches, repository settings, and Pages configuration through infra/repos/tf. It SHALL NOT generate a second inventory from the build-project registry. Its provider instance SHALL require exactly one configured GitHub organization.

  • WHEN the package renders its repository resources
  • THEN every catalog GitHub repository is managed from its catalog record

Dedicated per-project landing repositories SHALL NOT be managed. The main site publishes every project landing at /projects/<name>/, so the catalog carries no landing_project key and no per-project Pages repository, custom domain, github-pages environment, or landing default branch is declared.

  • WHEN a project’s landing page is published
  • THEN it is served by the apex site at /projects/<name>/ from the apex Pages repository
  • AND no per-project repository, Pages configuration, or custom domain is managed

The package SHALL provide import blocks for existing organization settings, repositories, default branches, memberships, developer collaborators, and the two existing src rulesets. Repositories with observed catalog IDs SHALL retain those IDs. Managed resources SHALL reject Terraform destruction, and any deletion or replacement SHALL require explicit user approval before apply.

The organization settings resource SHALL own only catalog-defined base repository access. Its mandatory billing argument SHALL be an import-only placeholder whose imported value is ignored; billing, profile, and other organization defaults SHALL remain outside this module’s ownership. Existing Dependabot alert settings SHALL be preserved.

  • WHEN import cannot resolve an observed catalog repository
  • THEN the operation fails rather than creating a replacement repository

Catalog administrators SHALL be organization owners. Catalog developers SHALL receive explicit write access on every GitHub catalog repository through non-authoritative collaborator resources. Repository default branches SHALL have active rulesets restricting creation, updates, deletion, and force pushes, with bypass restricted to organization administrators. The update restriction SHALL NOT permit fork syncing as a developer bypass. Ruleset application SHALL follow the managed default-branch selection. The two pre-existing src rulesets SHALL be imported unchanged, retaining their rules and bypass actors alongside the new default-branch ruleset.

  • WHEN a catalog developer writes to a non-protected contribution branch
  • THEN repository write access permits the contribution and opening a pull request
  • AND the developer cannot push or merge into the default branch
  • WHEN the site publisher writes content to the apex repository’s pages branch
  • THEN repository write access permits publication
  • AND its master default branch remains protected from developer pushes and merges

This package SHALL own repository resources and Pages settings. The documented workflow SHALL use //projects/alwaldend.com:deploy for site content and //infra/dns for DNS records.

  • WHEN an operator prepares an authorized site rollout
  • THEN repository and Pages changes are reviewed in the Terraform workflow
  • AND built content publication and DNS changes use their separately owned workflows

GitHub issues the custom-domain certificate as part of its Pages build and offers no direct reissue action, so the package SHALL document a supported, separately reviewed way to omit the apex site’s Pages block and then restore it, forcing a new certificate. The procedure SHALL use two filtered applies and SHALL include an HTTPS verification step.

  • WHEN an authorized apply removes the apex site’s Pages block
  • THEN the reviewed plan removes only that configuration
  • AND the documented procedure restores it with a later apply
  • AND the operator verifies the certificate served for the custom domain

10 - GitLab

GitLab resource management

This project owns GitLab groups, memberships, repository imports, and forks through the Terraform module. Organization membership and repository definitions come from the shared repository catalog.

10.1 - GitLab Terraform

GitLab groups, memberships, repository imports, and forks

This module manages the GitLab groups, memberships, repository imports, and forks declared in the shared repository catalog. Its catalog module supplies names, source URLs, and repository settings. Provider versions and checksums are recorded in the shared provider declarations.

Catalog administrators receive the Owner role; catalog users receive the Developer role. Group defaults permit Maintainers and Owners to push and merge into default branches, with force pushes and developer initial pushes disabled. Developers can read repositories, create feature branches, and open merge requests. Existing higher direct or inherited grants must be reviewed separately because GitLab retains a user’s highest access level.

GitHub copies use one-time Git imports, preserving their source default branches. They have no ongoing synchronization. GitLab forks retain their upstream fork relationship and target upstream when opening merge requests. The catalog owns destination names and upstream identities.

AL configuration authenticates with the src_infra_gitlab AppRole, injects the gitlab_token field as GITLAB_TOKEN, and supplies the existing Vault HTTP state backend. The configuration owns the Vault paths and plugin labels.

The provider uses GitLab.com by default. For another instance, set GITLAB_BASE_URL to its API endpoint with a trailing slash, such as https://gitlab.example.com/api/v4/; see the provider configuration.

The BUILD file exposes the standard tf.* commands and tf_tests.fmt_test; a plan uses live Vault and GitLab access.

The import blocks adopt existing catalog groups, projects, and their direct memberships. Membership discovery reads the catalog’s existing group IDs independently of the managed group resource, allowing a newly added member to be created. GitLab.com top-level groups must exist before Terraform can manage them. Group, membership, and project resources have prevent_destroy enabled.

For an existing destination project, record its observed numeric ID as gitlab.id in its catalog file. The project import block adopts that identity, and a postcondition rejects an unexpected ID. Do not create a replacement for an existing project or fork. Review the full plan for deletions and replacements before every apply; either requires explicit authorization for that scope.

Adopt default-branch protections in two phases:

  1. A new project starts without gitlab.id. Apply the reviewed full plan for groups, memberships, and projects. Wait for each repository import or fork to finish, then verify its default branch and effective protection against the catalog and group defaults.
  2. Record the observed project ID in the catalog after verifying its protection. The existing protection declarations select only projects with recorded IDs and import <project-id>:<default-branch>. This keeps new project creation independent of import IDs that are unknown until creation finishes. Review and apply the full plan that imports or updates those protections. Existing protections must never enter the provider’s Create operation: it can unprotect and recreate an existing default-branch rule.

Keep the original protection in place if it cannot be imported or its access levels need a replacement. The pinned provider supports in-place allowed_to_push and allowed_to_merge updates on GitLab.com; its scalar push_access_level and merge_access_level fields can require replacement. See the provider’s protection behavior.

Confirm that no overlapping project or group rule grants developers access to a default branch. A restrictive rule does not override a more permissive matching rule; see GitLab protected branches.

11 - harbor

Harbor deployment

This archived diagram preserves the historical source topology. The Mermaid source is authoritative; Bazel renders its light and dark SVGs.

Archive/Harbor architectureArchive/Harbor architecture

  • Go to User Profile, copy CLI secret
  • Run:
    podman login harbor.alwaldend.com
    
  • Generate approle secret id:
    SECRET_ID=$(bazel run //infra/harbor/cl:vault.secret_id | jq -r .data.secret_id)
    echo "Secret id: ${SECRET_ID}"
    
  • Patch secret-id in ./cl/cert-manager/issuer-approle.yaml
  • Encrypt:
    bazel run //infra/flux/cl:sops.encrypt infra/harbor/cl/cert-manager/issuer-approle.yaml
    

11.1 - Ansible

Ansible setup

11.2 - cl

Cluster manifests

11.3 - OpenSpec

11.3.1 - Specifications

11.3.1.1 - Infra harbor

11.3.1.1.1 - Harbor infrastructure specification

Describe the Harbor registry infrastructure owned by infra/harbor, spanning Proxmox setup, K3s host configuration, Flux-managed chart declarations, and Harbor service Terraform. The tf_setup, ansible, cl, and tf packages are implementation stages of this owner. This is a checked-in source baseline, without an observation of live registry availability or deployment success.

Baseline revision: 550d7e79b1f5fdbc2b6017b75178471d6914082f. Observed: 2026-09-08. Sources: owner README, BUILD, and the implementation links below.

Setup SHALL declare a Proxmox VM in the src_infra_harbor pool with two cores, 4096 MiB memory, and 20 GiB boot, 20 GiB K3s data, and 40 GiB K3s storage disks. Ansible SHALL apply the shared host and K3s roles with K3s secrets encryption enabled. Its firewall variables SHALL scope API port 6443 access to the declared Flux host address.

Sources: VM definition, deployment playbook, K3s configuration, and host variables.

  • WHEN the Harbor host configuration is evaluated
  • THEN the API-port firewall entry identifies the Flux source address
  • AND the K3s configuration enables secrets encryption and disables bundled Traefik

The Harbor Flux manifests SHALL declare a remote-cluster Kustomization and HelmRelease using src-infra-harbor-cl-kubeconfig. The HelmRelease SHALL pin its chart version, enable Helm tests and drift detection, set https://harbor.alwaldend.com as the external URL, and use the local-path storage class for its declared persistent components.

Source: Harbor Flux manifests.

  • WHEN a maintainer evaluates the Harbor HelmRelease
  • THEN the source selects a specific chart version and the remote Harbor cluster configuration
  • AND tests, drift detection, external URL, and persistent storage choices are explicit

Harbor service Terraform SHALL configure Vault OIDC authentication using client credentials from the Vault provider, the groups claim, and the src_infra_harbor_admins administrator group. It SHALL enable automatic onboarding and use the username claim for users.

Source: authentication configuration. The baseline sets oidc_verify_cert = false with a CA-issues comment; this specification does not claim that OIDC server certificate verification is enabled.

  • WHEN the service Terraform authentication resource is inspected
  • THEN it references Vault-provided client credentials and the configured user and group claims
  • AND its certificate-verification setting remains visible as a baseline limitation

Service Terraform SHALL declare an alwaldend project with vulnerability scanning and automatic SBOM generation, plus a dockerhub proxy-cache project linked to the Docker Hub registry. Both projects SHALL grant the configured src_infra_harbor_users OIDC group the guest role.

Sources: first-party project and Docker Hub proxy.

  • WHEN the managed registry projects are evaluated
  • THEN alwaldend enables scanning and SBOM generation, while dockerhub references the Docker Hub proxy registry
  • AND both group membership resources specify guest access

11.3.1.2 - Owned dns

11.3.1.2.1 - owned-dns Specification

Define Harbor DNS management through the owner’s tf_setup root, including canonical declarations, scoped credentials, and offline source checks before adopting live records.

The tf_setup root SHALL consume this owner’s canonical dnsconfig.json through the shared DNS Terraform module, preserving declared record identities and views. DNS resources SHALL default to disabled before adoption and SHALL default to enabled in the reviewed adoption revision and after adoption. Adoption SHALL import the exact existing provider records and require a scoped no-change import plan before saved-plan apply. Subsequent reconciliation with unchanged root inputs SHALL preserve adopted records, unrelated DNS records, and non-DNS resources in the root.

  • WHEN the checked-in root is evaluated with default inputs before adoption
  • THEN it reads this owner’s declaration and disables managed DNS records
  • AND the package contains the module and declaration inputs
  • WHEN the reviewed adoption revision or an adopted root is evaluated with default inputs
  • THEN it reads this owner’s declaration and enables managed DNS records
  • AND enabled ownership remains the default after import
  • WHEN this owner applies a reviewed saved import plan through //infra/harbor/tf_setup:dns.apply
  • THEN each existing provider record is imported at its exact owner-local address
  • AND a reviewed adoption plan proposes no record additions, changes, replacements, or deletions before apply
  • WHEN the adopted root reconciles unchanged canonical declarations and root inputs
  • THEN the plan proposes no changes to the adopted DNS records
  • AND unrelated DNS records and non-DNS resources in the root remain unchanged

The DNS wrappers SHALL select src_infra_harbor through the repository AL flow and keep secret values in injected variables. The dns.plan, dns.show, and dns.apply targets SHALL select dns=1 and retain the owner’s setup backend; plans SHALL target module.dns and apply SHALL require a reviewed saved plan. Ordinary setup wrappers SHALL retain their service authentication behavior. Real DNS credentials and Vault policy grants SHALL be prerequisites for operational Terraform calls. RouterOS DNS credentials SHALL remain isolated from unrelated RouterOS resources. The package SHALL expose a format test without live credentials. DNS-targeted validation SHALL NOT establish unrelated VM or service health.

  • WHEN the package format test executes
  • THEN it checks the packaged Terraform configuration without live credentials
  • AND existing non-DNS authentication and backend paths remain unchanged
  • WHEN an authorized operator selects the owner’s DNS wrapper
  • THEN it selects DNS credential injection through the owner AppRole and setup backend
  • AND its scoped plan and saved-plan apply cover DNS and its dependencies
  • AND disabled DNS resources do not imply offline provider configuration

11.4 - Tf

Terraform config

11.5 - Tf setup

Terraform setup

This root owns the Harbor DNS declaration. DNS ownership defaults to enabled after verified adoption; keep it enabled to retain the managed records.

Use //infra/harbor/tf_setup:dns.plan, :dns.show, and :dns.apply for scoped DNS reconciliation. These targets retain the setup backend and src_infra_harbor AppRole, select dns=1, and target module.dns. Apply requires a reviewed saved plan. Reconciliation of unchanged declarations must produce no DNS changes.

The ordinary setup targets retain their service authentication behavior. DNS checks do not establish PVE VM or service health; current aggregate source validation remains part of repository delivery.

12 - ingress

ingress

This diagram describes the maintained architecture source, not live inventory or service health. The Mermaid source is authoritative; Bazel renders its light and dark SVGs.

Ingress architectureIngress architecture

bazel run //infra/ingress/ansible

Services in traefik_services must use a backend target that resolves from the ingress hosts without resolving back to ingress. Define a dedicated site-local address, conventionally prefixed with dc1., in the owning service’s DNS configuration and use that address as the target.

bazel run //infra/ingress/tf
bazel run infra/ingress/tf -- -replace 'yandex_compute_disk.vpc["*"]'
  • Regenerate private and public keys:
    wg genkey | tee host1.privatekey.txt | wg pubkey >host1.publickey.txt
    wg genkey | tee host2.privatekey.txt | wg pubkey >host2.publickey.txt
    wg genkey | tee router.privatekey.txt | wg pubkey >router.publickey.txt
    cat - >data.json <<EOF
    {
      "wg_public_keys": {
        "host1": "$(cat host1.publickey.txt)",
        "host2": "$(cat host2.publickey.txt)",
        "router": "$(cat router.publickey.txt)"
      },
      "wg_private_keys": {
        "host1": "$(cat host1.privatekey.txt)",
        "host2": "$(cat host2.privatekey.txt)",
        "router": "$(cat router.privatekey.txt)"
      },
      "wg_preshared_keys": {
        "host1": "$(openssl rand 32 | base64)",
        "host2": "$(openssl rand 32 | base64)"
      }
    }
    EOF
    bazel run infra/ingress:vault.kv_put -- -format json alwaldend.com/vault1/approles/src_infra_ingress/wireguard "@${PWD}/data.json"
    rm data.json *.privatekey.txt *.publickey.txt
    

12.1 - Ansible

Ansible setup

12.2 - OpenSpec

12.2.1 - Specifications

12.2.1.1 - Infra ingress

12.2.1.1.1 - Infrastructure ingress

Describe Yandex Cloud ingress provisioning, the RouterOS WireGuard connection, and the Ansible-managed Traefik configuration. The baseline covers checked-in desired state, not a live routing or availability observation.

Baseline source revision: 550d7e79b1f5fdbc2b6017b75178471d6914082f. Observation date: 2026-09-08. Sources are linked in full; no excerpts are used.

Sources: component contract, VM resources, WireGuard resources, deployment playbook, Traefik entry points, and Traefik routes.

Terraform SHALL create addresses, encrypted boot disks, and instances for the active local.vpc entries, using the shared cloud-init source. The baseline active map SHALL contain host1 in ru-central1-d; the commented host2 entry SHALL NOT be treated as an enabled resource declaration.

  • WHEN the checked-in ingress Terraform configuration is evaluated
  • THEN instance resources are declared for host1
  • AND its boot disk references the declared KMS symmetric key
  • AND instance metadata includes the shared cloud-init content

The RouterOS configuration SHALL declare the ingress-vpc interface and a peer for each active ingress host. Peer endpoints SHALL use the corresponding cloud address, and allowed addresses SHALL come from the Ansible inventory’s wg_ip.

  • WHEN an ingress host is present in the active Terraform host map
  • THEN its WireGuard peer uses the host’s declared public and preshared key variables and inventory address
  • AND the interface belongs to the configured LAN, forwarding, and ICMP lists

Each traefik_services backend target MUST resolve from the ingress hosts without resolving back to ingress. The service owner SHALL declare the dedicated site-local address. Generated HTTPS backend transports SHALL use the service hostname as TLS server name.

  • WHEN a service is added to traefik_services
  • THEN its host route forwards to https:// followed by its backend target
  • AND that target is a dedicated site-local address defined by the service owner
  • AND backend TLS uses the service’s configured hostname

The ingress playbook SHALL apply the host, WireGuard, and Traefik roles. The Traefik templates SHALL redirect the web entry point to HTTPS and configure default TLS client authentication as RequireAndVerifyClientCert using the configured client CA file.

  • WHEN the Ansible role renders the checked-in Traefik templates
  • THEN port 80 redirects to the secure entry point
  • AND the default TLS options require a certificate verified against the configured client CA

12.2.1.2 - Owned dns

12.2.1.2.1 - owned-dns Specification

Define retained ingress DNS ownership through the owner’s tf root, including canonical declarations, scoped execution, and offline source checks.

The tf root SHALL consume this owner’s canonical dnsconfig.json through the shared DNS Terraform module, preserving declared record identities and views. DNS resources SHALL default to disabled until the authorized adoption revision and SHALL retain enabled ownership after existing records are imported into the owner’s state. Reconciliation against unchanged declarations and adopted state SHALL propose no record additions, changes, replacements, or deletions.

  • WHEN the checked-in root is evaluated with default inputs before its authorized adoption revision
  • THEN it reads this owner’s declaration and disables managed DNS records
  • AND the package contains the module and declaration inputs
  • WHEN authorized adoption records the shared writer audit and control coverage and enables the owner’s record management
  • THEN the owning root imports existing records by exact provider ID into its state while preserving their declared identities and views
  • WHEN the adopted root uses its checked-in source defaults
  • THEN DNS ownership remains enabled and the package retains the module and canonical declaration inputs
  • WHEN the owning root plans against its adopted state and unchanged declarations
  • THEN it proposes no record additions, changes, replacements, or deletions

The DNS wrappers SHALL select src_infra_ingress through the repository AL flow and keep secret values in injected variables. dns.plan, dns.show, and dns.apply SHALL retain the owning root and backend, select dns=1, and scope planning to module.dns. Apply SHALL require the reviewed saved plan. This workflow SHALL preserve ordinary service authentication and SHALL NOT establish overall service health. Required DNS credentials and Vault grants SHALL remain operational prerequisites, and RouterOS DNS credentials SHALL remain isolated from unrelated RouterOS resources. The package SHALL expose a format test without Vault authentication or DNS provider access.

  • WHEN the package format test executes
  • THEN it checks the packaged Terraform configuration without live credentials
  • AND existing non-DNS authentication and backend paths remain unchanged
  • WHEN the operator selects the owning DNS wrappers
  • THEN they select DNS credential injection through the owner AppRole with dns=1
  • AND their scoped plan validates DNS and its dependencies
  • AND disabled DNS resources do not imply offline provider configuration

12.3 - Tf

Terraform config

Use this package’s dns.plan, dns.show, and dns.apply targets for the scoped DNS adoption workflow.

DNS ownership is enabled by default after the verified 2026-09-13 adoption. The adoption evidence records four imports, a no-change follow-up plan, and preserved provider inventories. The DNS wrappers select dns=1 and module.dns within this root and backend; their validation covers DNS and its dependencies. Apply requires a reviewed saved plan, and ordinary ingress service authentication remains separate.

13 - Mikrotik

Mikrotik setup for dc1.alwaldend.com

The Terraform package prepares owner-local DNS management. Router exports remain documentation inputs and are not applied by that package.

This diagram describes the maintained architecture source, not live inventory or service health. The Mermaid source is authoritative; Bazel renders its light and dark SVGs.

DC1 architectureDC1 architecture

  • Open Winbox
  • Open new terminal
  • Run /export
  • Copy output
/interface bridge
add admin-mac=78:9A:18:38:6C:CA auto-mac=no comment="bridge1 (wired)" name=bridge1
add comment="bridge2 (wireless)" name=bridge2
/interface wifi
set [ find default-name=wifi1 ] channel.frequency=5000-5400 .skip-dfs-channels=10min-cac comment="wifi1 (5GHz)" configuration.country=Russia .mode=ap .ssid=divinity-5GHz datapath.client-isolation=yes disabled=no security.authentication-types=wpa2-psk,wpa3-psk .connect-priority=0 .ft=yes .ft-over-ds=yes
set [ find default-name=wifi2 ] channel.skip-dfs-channels=10min-cac comment="wifi2 (2GHz)" configuration.country=Russia .mode=ap .ssid=divinity-2GHz datapath.client-isolation=yes disabled=no security.authentication-types=wpa2-psk,wpa3-psk .connect-priority=0 .ft=yes .ft-over-ds=yes
/interface ethernet
set [ find default-name=ether1 ] comment=ether1 l2mtu=1500 mac-address=F4:28:53:7F:A4:59
set [ find default-name=ether2 ] comment=ether2
set [ find default-name=ether3 ] comment=ether3
set [ find default-name=ether4 ] comment=ether4
set [ find default-name=ether5 ] comment=ether5
/interface wireguard
add comment="tf[users/simeonwarren/hermes/tf_setup]" disabled=yes listen-port=13232 mtu=1420 name=hermes-vpc
add comment="tf[infra/ingress/tf]" listen-port=13231 mtu=1420 name=ingress-vpc
/interface ethernet switch
set switch1 cpu-flow-control=yes
/interface list
add comment=defconf name=WAN
add comment=defconf name=LAN
add name=accept-forward-WAN
add name=accept-input-DNS
add name=accept-input-DHCP-server
add name=accept-input-ICMP
add name=accept-input-winbox
add name=accept-input-web-ui
add name=accept-input-mikrotik-neighbor-discovery
add name=accept-forward-LAN
add name=accept-output-LAN
add name=accept-input-NTP
add name=accept-input-API
/ip pool
add comment=bridge1 name=bridge1 ranges=192.168.1.10-192.168.1.254
add comment=bridge2 name=bridge2 ranges=192.168.2.10-192.168.2.254
/ip dhcp-server
add address-pool=bridge1 comment=bridge1 interface=bridge1 lease-time=10m name=bridge1
add address-pool=bridge2 interface=bridge2 name=bridge2
/ipv6 pool
add name=dc01 prefix=fd2e:546d:5738::/48 prefix-length=64
/user group
add comment=src_infra_dns name=src_infra_dns policy=read,write,api,rest-api,!local,!telnet,!ssh,!ftp,!reboot,!policy,!test,!winbox,!password,!web,!sniff,!sensitive,!romon
add comment=src_infra_ingress name=src_infra_ingress policy=read,write,api,rest-api,!local,!telnet,!ssh,!ftp,!reboot,!policy,!test,!winbox,!password,!web,!sniff,!sensitive,!romon
add comment=users_simeonwarren name=users_simeonwarren policy=read,write,api,rest-api,!local,!telnet,!ssh,!ftp,!reboot,!policy,!test,!winbox,!password,!web,!sniff,!sensitive,!romon
/interface bridge port
add bridge=bridge1 comment=bridge1-ether2 interface=ether2
add bridge=bridge1 comment=bridge1-ether3 interface=ether3
add bridge=bridge1 comment=bridge1-ether4 interface=ether4
add bridge=bridge1 comment=bridge1-ether5 interface=ether5
add bridge=bridge2 comment=bridge2-wifi1 interface=wifi1
add bridge=bridge2 comment=bridge2-wifi2 interface=wifi2
/ip neighbor discovery-settings
set discover-interface-list=LAN
/interface detect-internet
set detect-interface-list=WAN
/interface list member
add interface=bridge1 list=LAN
add interface=ether1 list=WAN
add interface=bridge2 list=LAN
add interface=bridge2 list=accept-forward-WAN
add interface=bridge1 list=accept-forward-WAN
add interface=bridge1 list=accept-input-DNS
add interface=bridge2 list=accept-input-DNS
add interface=bridge1 list=accept-input-DHCP-server
add interface=bridge2 list=accept-input-DHCP-server
add interface=bridge1 list=accept-input-ICMP
add interface=bridge2 list=accept-input-ICMP
add interface=bridge1 list=accept-input-winbox
add interface=bridge1 list=accept-input-web-ui
add interface=bridge1 list=accept-input-mikrotik-neighbor-discovery
add interface=bridge1 list=accept-forward-LAN
add interface=bridge1 list=accept-output-LAN
add interface=bridge1 list=accept-input-NTP
add interface=bridge1 list=accept-input-API
add comment="tf[infra/ingress/tf]" interface=ingress-vpc list=accept-input-ICMP
add comment="tf[infra/ingress/tf]" interface=ingress-vpc list=LAN
add comment="tf[infra/ingress/tf]" interface=ingress-vpc list=accept-forward-LAN
add comment="tf[users/simeonwarren/hermes/tf_setup]" interface=hermes-vpc list=accept-forward-LAN
add comment="tf[users/simeonwarren/hermes/tf_setup]" interface=hermes-vpc list=accept-input-ICMP
add comment="tf[users/simeonwarren/hermes/tf_setup]" interface=hermes-vpc list=LAN
/interface ovpn-server server
add mac-address=FE:B3:B4:C4:A4:48 name=ovpn-server1
/interface wireguard peers
add allowed-address=10.10.0.2/24 comment=host2 endpoint-address=103.76.53.6 endpoint-port=51820 interface=ingress-vpc name=ingress-vpc-host2 persistent-keepalive=5s public-key="Z2JamOjZYOGaf4tPZzchyHjLw/XlOtUtQObyROEQ9DM="
add allowed-address=10.10.0.1/24 comment=host1 endpoint-address=158.160.196.128 endpoint-port=51820 interface=ingress-vpc name=ingress-vpc-host1 persistent-keepalive=5s public-key="xmyl+frvngmzRB9z5yEURxQj4vTw47tKQV7EZrTAREw="
add allowed-address=10.20.0.1/24 comment=host1 endpoint-address=158.160.220.223 endpoint-port=51820 interface=hermes-vpc name=hermes-vpc-host1 persistent-keepalive=5s public-key="oA4ZpsmrclIOIWh3ECsb4ZFKH1hQMDtuW3xNXat3IyQ="
/ip settings
set send-redirects=no
/ip address
add address=192.168.1.1/24 comment="bridge1 (LAN)" interface=bridge1 network=192.168.1.0
add address=192.168.2.1/24 comment="bridge2 (Wireless)" interface=bridge2 network=192.168.2.0
add address=192.168.10.1/24 comment=host1.pve1.dc1.alwaldend.com interface=bridge1 network=192.168.10.0
add address=10.10.0.0/24 comment="tf[infra/ingress/tf]" interface=ingress-vpc network=10.10.0.0
add address=10.20.0.0/24 comment="tf[users/simeonwarren/hermes/tf_setup]" interface=hermes-vpc network=10.20.0.0
/ip dhcp-client
add comment=defconf interface=ether1 name=ether1 use-peer-dns=no
/ip dhcp-server lease
add address=192.168.1.250 client-id=1:2c:cf:67:67:b5:13 mac-address=2C:CF:67:67:B5:13 server=bridge1
add address=192.168.1.218 client-id=1:e0:be:3:2b:9a:1a mac-address=E0:BE:03:2B:9A:1A server=bridge1
add address=192.168.1.216 client-id=ff:60:8:6d:aa:0:1:0:1:31:93:31:1a:34:5a:60:8:6d:aa mac-address=34:5A:60:08:6D:AA server=bridge1
/ip dhcp-server network
add address=192.168.1.0/24 comment=defconf dns-server=192.168.1.1 gateway=192.168.1.1
add address=192.168.2.0/24 dns-server=192.168.2.1 gateway=192.168.2.1
/ip dns
set allow-remote-requests=yes servers=1.1.1.2,1.0.0.2 use-doh-server=https://odoh.cloudflare-dns.com/dns-query verify-doh-cert=yes
/ip dns static
add address=185.199.108.153 name=alwaldend.com ttl=5m type=A
add address=185.199.109.153 name=alwaldend.com ttl=5m type=A
add address=185.199.110.153 name=alwaldend.com ttl=5m type=A
add address=185.199.111.153 name=alwaldend.com ttl=5m type=A
add address=2606:50c0:8000::153 name=alwaldend.com ttl=5m type=AAAA
add address=2606:50c0:8001::153 name=alwaldend.com ttl=5m type=AAAA
add address=2606:50c0:8002::153 name=alwaldend.com ttl=5m type=AAAA
add address=2606:50c0:8003::153 name=alwaldend.com ttl=5m type=AAAA
add mx-exchange=mail.protonmail.ch mx-preference=10 name=alwaldend.com ttl=5m type=MX
add mx-exchange=mailsec.protonmail.ch mx-preference=20 name=alwaldend.com ttl=5m type=MX
add name=alwaldend.com text="_globalsign-domain-verification=0QBJgVV_uwcFLTi1Rot3bb1LyJ5uW1WD0ygvIS4OM5" ttl=5m type=TXT
add name=alwaldend.com text="protonmail-verification=bdcd133d3f472fa17f66328950d02fbeae1bef75" ttl=5m type=TXT
add name=alwaldend.com text="v=spf1 include:_spf.protonmail.ch ~all" ttl=5m type=TXT
add name=_dmarc.alwaldend.com text="v=DMARC1; p=quarantine; adkim=s" ttl=5m type=TXT
add cname=protonmail.domainkey.djgwfzcu5fgjtpoijqqomgifmqj6zeiuwdd4mzim4hrxab3zsgwkq.domains.proton.ch name=protonmail._domainkey.alwaldend.com ttl=5m type=CNAME
add cname=protonmail2.domainkey.djgwfzcu5fgjtpoijqqomgifmqj6zeiuwdd4mzim4hrxab3zsgwkq.domains.proton.ch name=protonmail2._domainkey.alwaldend.com ttl=5m type=CNAME
add cname=protonmail3.domainkey.djgwfzcu5fgjtpoijqqomgifmqj6zeiuwdd4mzim4hrxab3zsgwkq.domains.proton.ch name=protonmail3._domainkey.alwaldend.com ttl=5m type=CNAME
add address=192.168.1.222 name=bm1.dc1.alwaldend.com ttl=5m type=A
add address=192.168.1.216 name=bm2.dc1.alwaldend.com ttl=5m type=A
add address=fd2e:546d:5738:0:365a:60ff:fe08:6daa name=bm2.dc1.alwaldend.com ttl=10m type=AAAA
add address=192.168.1.218 name=bm3.dc1.alwaldend.com ttl=5m type=A
add address=fd2e:546d:5738:0:e2be:3ff:fe2b:9a1a name=bm3.dc1.alwaldend.com ttl=10m type=AAAA
add cname=bm2.dc1.alwaldend.com name=host1.pve1.dc1.alwaldend.com ttl=10m type=CNAME
add address=192.168.10.10 name=cloudinit-test.vm.pve1.dc1.alwaldend.com ttl=5m type=A
add address=192.168.1.1 name=router1.dc1.alwaldend.com ttl=5m type=A
add address=fd2e:546d:5738::1 name=router1.dc1.alwaldend.com ttl=10m type=AAAA
add address=192.168.1.254 name=switch1.dc1.alwaldend.com ttl=5m type=A
add address=192.168.1.218 name=vault.dc1.alwaldend.com ttl=5m type=A
add mx-exchange=mx1.simplelogin.co mx-preference=10 name=simplelogin.alwaldend.com ttl=3h type=MX
add mx-exchange=mx2.simplelogin.co mx-preference=20 name=simplelogin.alwaldend.com ttl=3h type=MX
add name=simplelogin.alwaldend.com text="sl-verification=bxfzzfjiggzsxyzxhhmkmjqkaskjgy" ttl=3h type=TXT
add name=simplelogin.alwaldend.com text="v=spf1 include:simplelogin.co ~all" ttl=3h type=TXT
add name=_dmarc.simplelogin.alwaldend.com text="v=DMARC1; p=quarantine; pct=100; adkim=s; aspf=s" ttl=3h type=TXT
add cname=dkim._domainkey.simplelogin.co name=dkim._domainkey.simplelogin.alwaldend.com ttl=3h type=CNAME
add cname=dkim02._domainkey.simplelogin.co name=dkim02._domainkey.simplelogin.alwaldend.com ttl=3h type=CNAME
add cname=dkim03._domainkey.simplelogin.co name=dkim03._domainkey.simplelogin.alwaldend.com ttl=3h type=CNAME
add cname=alwaldend.com name=www.alwaldend.com ttl=5m type=CNAME
add mx-exchange=mx.yandex.net mx-preference=10 name=yandex.alwaldend.com ttl=6h type=MX
add name=yandex.alwaldend.com text="v=spf1 redirect=_spf.yandex.net" ttl=5m type=TXT
add name=yandex.alwaldend.com text="yandex-verification: b83672f59b3dbe16" ttl=5m type=TXT
add name=mail._domainkey.yandex.alwaldend.com text="v=DKIM1; k=rsa; t=s; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCcYzFVgkeDOhaIIkWM8gNQjxVsv0/aXfU+ax5urB5y6hA6lSjRnjRo6tm0bXbkOJf41GmiwMNgdXpwRtzgzAlX1i2aJbtEr4b9jzibEGLQ7Cvqs44bOYES9f/K3ueQpnvdTOJmFqlRReFL7ZrUyDFCoQ7f4+7h4i8s01cCcRrt5wIDAQAB" ttl=5m type=TXT
add address=192.168.1.218 name=host1.vault.dc1.alwaldend.com ttl=5m type=A
add address=192.168.10.60 name=flux.alwaldend.com ttl=5m type=A
add address=192.168.10.60 name=host1.flux.alwaldend.com ttl=5m type=A
add cname=flux.alwaldend.com name=openid.flux.alwaldend.com ttl=10m type=CNAME
add cname=flux.alwaldend.com name=operator.flux.alwaldend.com ttl=10m type=CNAME
add address=192.168.10.40 name=forgejo.alwaldend.com ttl=5m type=A
add address=192.168.10.40 name=host1.forgejo.alwaldend.com ttl=5m type=A
add address=192.168.10.50 name=harbor.alwaldend.com ttl=5m type=A
add address=192.168.10.50 name=host1.harbor.alwaldend.com ttl=5m type=A
add address=192.168.1.216 name=pve.alwaldend.com ttl=5m type=A
add address=192.168.10.80 name=threexui.alwaldend.com ttl=5m type=A
add address=192.168.10.80 name=host1.threexui.alwaldend.com ttl=5m type=A
add address=45.142.141.133 name=njalla1.nodes.threexui.alwaldend.com ttl=5m type=A
add address=2a0a:3840:8078:141:0:2d8e:8d85:1337 name=njalla1.nodes.threexui.alwaldend.com ttl=10m type=AAAA
add address=192.168.1.218 name=vault.alwaldend.com ttl=5m type=A
add address=103.76.53.6 name=ingress.alwaldend.com ttl=5m type=A
add address=158.160.196.128 name=ingress.alwaldend.com ttl=5m type=A
add address=158.160.196.128 name=host1.ingress.alwaldend.com ttl=5m type=A
add address=103.76.53.6 name=host2.ingress.alwaldend.com ttl=5m type=A
add name=yc.threexui.alwaldend.com ns=ns1.yandexcloud.net ttl=5m type=NS
add name=yc.threexui.alwaldend.com ns=ns2.yandexcloud.net ttl=5m type=NS
add cname=host1.nodes.yc.threexui.alwaldend.com name=yc1.nodes.threexui.alwaldend.com ttl=10m type=CNAME
add address=192.168.10.100 name=runner1.forgejo-runner.alwaldend.com ttl=5m type=A
add cname=host1.yc.hermes.simeonwarren.users.alwaldend.com name=hermes.simeonwarren.users.alwaldend.com ttl=10m type=CNAME
add cname=host1.yc.hermes.simeonwarren.users.alwaldend.com name=host1.hermes.simeonwarren.users.alwaldend.com ttl=10m type=CNAME
add name=yc.hermes.simeonwarren.users.alwaldend.com ns=ns1.yandexcloud.net ttl=5m type=NS
add name=yc.hermes.simeonwarren.users.alwaldend.com ns=ns2.yandexcloud.net ttl=5m type=NS
/ip firewall filter
add action=accept chain=input comment="defconf: accept established,related,untracked" connection-state=established,related,untracked
add action=drop chain=input comment="defconf: drop invalid" connection-state=invalid log-prefix=drop-invalid
add action=accept chain=input comment="defconf: accept ICMP" in-interface-list=accept-input-ICMP protocol=icmp
add action=accept chain=input comment="defconf: accept to local loopback (for CAPsMAN)" dst-address=127.0.0.1
add action=drop chain=input comment="defconf: drop all not coming from LAN" in-interface-list=!LAN log-prefix=drop-not-coming-from-lan
add action=accept chain=forward comment="defconf: accept in ipsec policy" ipsec-policy=in,ipsec
add action=accept chain=forward comment="defconf: accept out ipsec policy" ipsec-policy=out,ipsec
add action=fasttrack-connection chain=forward comment="defconf: fasttrack" connection-state=established,related
add action=accept chain=forward comment="defconf: accept established,related, untracked" connection-state=established,related,untracked
add action=drop chain=forward comment="defconf: drop invalid" connection-state=invalid log-prefix=drop-invalid
add action=drop chain=forward comment="defconf: drop all from WAN not DSTNATed" connection-nat-state=!dstnat connection-state=new in-interface-list=WAN log-prefix=drop-from-wan-not-dstnated
add action=accept chain=input in-interface-list=WAN protocol=gre
add action=accept chain=forward comment="accept forward WAN" in-interface-list=accept-forward-WAN out-interface-list=WAN
add action=accept chain=forward comment="accept forward LAN" in-interface-list=accept-forward-LAN out-interface-list=LAN
add action=accept chain=input comment="accept input DNS (udp)" dst-port=53 in-interface-list=accept-input-DNS protocol=udp
add action=accept chain=input comment="accept input DNS (tcp)" dst-port=53 in-interface-list=accept-input-DNS protocol=tcp
add action=accept chain=input comment=accept-input-NTP dst-port=123 in-interface-list=accept-input-NTP protocol=udp
add action=accept chain=input comment="accept input DHCP-server" dst-port=67 in-interface-list=accept-input-DHCP-server log-prefix=accept-DHCP protocol=udp
add action=accept chain=input comment="accept input winbox (tcp)" dst-port=8291 in-interface-list=accept-input-winbox protocol=tcp
add action=accept chain=input comment="accept input winbox (udp)" dst-port=20561 in-interface-list=accept-input-winbox protocol=udp
add action=accept chain=input comment="accept input web ui" dst-port=80,443 in-interface-list=accept-input-web-ui protocol=tcp
add action=accept chain=input comment="accept input mikrotik neighbor discovery" dst-port=5678 in-interface-list=accept-input-mikrotik-neighbor-discovery protocol=udp
add action=drop chain=forward comment="drop forward" log=yes log-prefix=drop-forward
add action=drop chain=input comment="drop input" log=yes log-prefix=drop-input
add action=accept chain=output comment=accept-output-LAN out-interface-list=LAN
/ip firewall nat
add action=masquerade chain=srcnat comment="defconf: masquerade" ipsec-policy=out,none out-interface-list=WAN
/ip ipsec profile
set [ find default=yes ] dpd-interval=2m dpd-maximum-failures=5
/ip service
set www-ssl certificate=alwaldend.com_acme disabled=no
set reverse-proxy certificate=alwaldend.com_acme
set api-ssl certificate=alwaldend.com_acme
/ipv6 address
add address=::1 from-pool=dc01 interface=bridge1
add address=::1:0:0:0:1 from-pool=dc01 interface=bridge2
/ipv6 firewall address-list
add address=::/128 comment="defconf: unspecified address" list=bad_ipv6
add address=::1/128 comment="defconf: lo" list=bad_ipv6
add address=fec0::/10 comment="defconf: site-local" list=bad_ipv6
add address=::ffff:0.0.0.0/96 comment="defconf: ipv4-mapped" list=bad_ipv6
add address=::/96 comment="defconf: ipv4 compat" list=bad_ipv6
add address=100::/64 comment="defconf: discard only " list=bad_ipv6
add address=2001:db8::/32 comment="defconf: documentation" list=bad_ipv6
add address=2001:10::/28 comment="defconf: ORCHID" list=bad_ipv6
add address=3ffe::/16 comment="defconf: 6bone" list=bad_ipv6
/ipv6 firewall filter
add action=accept chain=input comment="defconf: accept established,related,untracked" connection-state=established,related,untracked
add action=drop chain=input comment="defconf: drop invalid" connection-state=invalid
add action=accept chain=input comment="defconf: accept ICMPv6" in-interface-list=accept-input-ICMP protocol=icmpv6
add action=accept chain=input comment="defconf: accept UDP traceroute" dst-port=33434-33534 protocol=udp
add action=accept chain=input comment="defconf: accept DHCPv6-Client prefix delegation." dst-port=546 protocol=udp src-address=fe80::/10
add action=accept chain=input comment="defconf: accept IKE" dst-port=500,4500 protocol=udp
add action=accept chain=input comment="defconf: accept ipsec AH" protocol=ipsec-ah
add action=accept chain=input comment="defconf: accept ipsec ESP" protocol=ipsec-esp
add action=accept chain=input comment="defconf: accept all that matches ipsec policy" ipsec-policy=in,ipsec
add action=drop chain=input comment="defconf: drop everything else not coming from LAN" in-interface-list=!LAN
add action=accept chain=forward comment="defconf: accept established,related,untracked" connection-state=established,related,untracked
add action=drop chain=forward comment="defconf: drop invalid" connection-state=invalid
add action=drop chain=forward comment="defconf: drop packets with bad src ipv6" src-address-list=bad_ipv6
add action=drop chain=forward comment="defconf: drop packets with bad dst ipv6" dst-address-list=bad_ipv6
add action=drop chain=forward comment="defconf: rfc4890 drop hop-limit=1" hop-limit=equal:1 protocol=icmpv6
add action=accept chain=forward comment="defconf: accept ICMPv6" in-interface-list=accept-input-ICMP protocol=icmpv6
add action=accept chain=forward comment="defconf: accept HIP" protocol=139
add action=accept chain=forward comment="defconf: accept IKE" dst-port=500,4500 protocol=udp
add action=accept chain=forward comment="defconf: accept ipsec AH" protocol=ipsec-ah
add action=accept chain=forward comment="defconf: accept ipsec ESP" protocol=ipsec-esp
add action=accept chain=forward comment="defconf: accept all that matches ipsec policy" ipsec-policy=in,ipsec
add action=drop chain=forward comment="defconf: drop everything else not coming from LAN" in-interface-list=!LAN
add action=accept chain=forward comment="accept forward WAN" in-interface-list=accept-forward-WAN out-interface-list=WAN
add action=accept chain=forward comment="accept forward LAN" in-interface-list=accept-forward-LAN out-interface-list=LAN
add action=accept chain=input comment="accept input DNS (udp)" dst-port=53 in-interface-list=accept-input-DNS protocol=udp
add action=accept chain=input comment="accept input DNS (tcp)" dst-port=53 in-interface-list=accept-input-DNS protocol=tcp
add action=accept chain=input comment=accept-input-NTP dst-port=123 in-interface-list=accept-input-NTP protocol=udp
add action=accept chain=input comment="accept input winbox (tcp)" dst-port=8291 in-interface-list=accept-input-winbox protocol=tcp
add action=accept chain=input comment="accept input winbox (udp)" dst-port=20561 in-interface-list=accept-input-winbox protocol=udp
add action=accept chain=input comment="accept input web ui" dst-port=80,443 in-interface-list=accept-input-web-ui protocol=tcp
add action=accept chain=input comment="accept input mikrotik neighbor discovery" dst-port=5678 in-interface-list=accept-input-mikrotik-neighbor-discovery protocol=udp
add action=drop chain=forward comment="drop forward" log=yes log-prefix=drop-forward-ipv6
add action=drop chain=input comment="drop input" log=yes log-prefix=drop-input-ipv6
add action=accept chain=output comment=accept-output-LAN out-interface-list=LAN
/ipv6 nd
set [ find default=yes ] advertise-dns=yes interface=bridge1
add advertise-dns=yes interface=bridge2
/system clock
set time-zone-name=Europe/Moscow
/system identity
set name=router1.dc1.alwaldend.com
/system ntp server
set enabled=yes
/system routerboard settings
set auto-upgrade=yes
/tool mac-server
set allowed-interface-list=LAN
/tool mac-server mac-winbox
set allowed-interface-list=LAN
# 2025-09-07 11:04:01 by RouterOS 7.19.4
# model = L009UiGS-2HaxD
/interface bridge
add name=bridge01
/port
set 0 name=serial0
/interface bridge port
add bridge=bridge01 interface=ether2
add bridge=bridge01 interface=ether3
add bridge=bridge01 interface=ether4
add bridge=bridge01 interface=ether5
add bridge=bridge01 interface=ether6
add bridge=bridge01 interface=ether7
add bridge=bridge01 interface=ether8
add bridge=bridge01 interface=ether1
/ip neighbor discovery-settings
set discover-interface-list=!dynamic
/ipv6 settings
set accept-router-advertisements=yes
/ip dhcp-client
add interface=bridge01
/system clock
set time-zone-name=Europe/Moscow
/system identity
set name=router02.dc01.alwaldend.com
/system routerboard settings
set enter-setup-on=delete-key

13.1 - DNS Terraform

This root packages the owner’s canonical DNS declaration through the shared DNS module and keeps state in the owner AppRole’s Vault HTTP backend. The root contains DNS resources only. dns_enabled defaults to true after verified adoption. Keep it enabled to retain existing records; disabling it would propose deletion.

Use the normal //infra/mikrotik/tf:tf.<operation> wrappers for this DNS-only root. The DNS migration procedure owns credential provisioning, reconciliation, and recovery. The adoption record contains the import and inventory-preservation evidence. The package exposes //infra/mikrotik/tf:tf_tests.fmt_test for offline formatting checks.

Operational Terraform calls require the DNS AppRole policy grants and credential fields described by the migration procedure. Provider configuration requires real credentials and live access independently of the activation setting.

13.2 - OpenSpec

13.2.1 - Specifications

13.2.1.1 - Infra mikrotik

13.2.1.1.1 - Infrastructure MikroTik

Describe the checked-in RouterOS export snapshots and site DNS declarations for the dc1 routers. This package is a documentation and record-input owner: its BUILD file declares no router deployment executable. Export contents are historical source evidence, not a verification of current device configuration.

Baseline source revision: 550d7e79b1f5fdbc2b6017b75178471d6914082f. Observation date: 2026-09-08. Sources are linked in full; no excerpts are used.

Sources: export workflow, package targets, router1 snapshot, router2 snapshot, and DNS declarations.

The package SHALL retain the router export files as documentation inputs and describe the RouterOS /export collection workflow. Documentation SHALL distinguish these snapshots from automatically applied desired state. The Terraform root SHALL manage DNS records only and SHALL NOT apply router exports.

  • WHEN a reader opens the component documentation
  • THEN the reader can inspect both router export snapshots
  • AND the Terraform package exposes owner-local DNS management while the snapshots remain documentary inputs

The baseline description SHALL identify router1’s separate wired and wireless bridges with their own DHCP networks, and router2’s single bridge containing its eight Ethernet interfaces with a DHCP client. It SHALL NOT infer additional live devices or enabled topology from comments elsewhere in the repository.

  • WHEN the two checked-in exports are compared
  • THEN router1 records bridge1 for wired ports and bridge2 for Wi-Fi ports
  • AND router2 records bridge01 and an IP DHCP client on that bridge

The router1 snapshot SHALL describe named interface lists for input and forwarding permissions, explicit terminal IPv4 input and forwarding drops, and source NAT masquerading for WAN egress. This contract describes the export and SHALL NOT be represented as a live firewall audit.

  • WHEN a reader follows the IPv4 firewall section of the snapshot
  • THEN the listed service permissions precede terminal input and forwarding drop rules
  • AND the NAT section declares masquerading for traffic leaving the WAN list

The package SHALL expose its dnsconfig.json as a Bazel filegroup. Its router, switch, and bare-metal records SHALL be assigned to the dc1 destination view.

  • WHEN the DNS owner consumes //infra/mikrotik:dnsconfig
  • THEN router1 IPv4 and IPv6 records and the declared switch and bare-metal IPv4 records are available for the dc1 view

13.2.1.2 - Owned dns

13.2.1.2.1 - owned-dns Specification

Define MikroTik router DNS management through the owner’s tf root, including canonical declarations, scoped credentials, and offline source checks before adopting live records.

The tf root SHALL consume this owner’s canonical dnsconfig.json through the shared DNS Terraform module, preserving declared record identities and views. DNS resources SHALL default to disabled until the authorized adoption revision and SHALL retain enabled ownership after existing records are imported into the owner’s state. Reconciliation against unchanged declarations and adopted state SHALL propose no record additions, changes, replacements, or deletions.

  • WHEN the checked-in root is evaluated with default inputs before its authorized adoption revision
  • THEN it reads this owner’s declaration and disables managed DNS records
  • AND the package contains the module and declaration inputs
  • WHEN the authorized adoption audits competing writers within its documented coverage, keeps identified competing writers stopped, and enables the owner’s record management
  • THEN the owning root imports existing records by exact provider ID into its state while preserving their declared identities and views
  • WHEN the adopted root uses its checked-in source defaults
  • THEN DNS ownership remains enabled and the package retains the module and canonical declaration inputs
  • WHEN the owning root plans against its adopted state and unchanged declarations
  • THEN it proposes no record additions, changes, replacements, or deletions

The DNS wrapper SHALL select src_infra_mikrotik through the repository AL flow and keep secret values in injected variables. DNS injection SHALL select the root’s existing Terraform stage label. Real DNS credentials and Vault policy grants SHALL be prerequisites for operational Terraform calls. RouterOS DNS credentials SHALL be isolated from unrelated RouterOS resources. The package SHALL expose a format test that does not authenticate to Vault or contact DNS providers.

  • WHEN the package format test executes
  • THEN it checks the packaged Terraform configuration without live credentials
  • AND existing non-DNS authentication and backend paths remain unchanged
  • WHEN the wrapper selects its Terraform stage labels
  • THEN it selects DNS credential injection through the owner AppRole
  • AND disabled DNS resources do not imply offline provider configuration

14 - Nas

TrueNAS deploy

The Terraform package prepares this owner’s DNS records through its dedicated Vault AppRole. NAS service provisioning remains outside that root.

This diagram describes the maintained architecture source, not live inventory or service health. The Mermaid source is authoritative; Bazel renders its light and dark SVGs.

Truenas architectureTruenas architecture

14.1 - DNS Terraform

This root packages the owner’s canonical DNS declarations through the shared DNS module and keeps state in the owner AppRole’s Vault HTTP backend. The root contains DNS resources only. dns_enabled defaults to true after verified adoption. Keep it enabled to retain existing records; disabling it would propose deletion.

The DNS migration procedure owns credential provisioning, reconciliation, and recovery. The adoption record contains import and verification evidence. Use the //infra/nas/tf:tf wrappers for operational commands and //infra/nas/tf:tf_tests.fmt_test for offline source validation.

Operational Terraform calls require the DNS AppRole policy grants and credential fields described by the migration procedure. Provider configuration and authentication require real credentials and live access.

14.2 - OpenSpec

14.2.1 - Specifications

14.2.1.1 - Infra nas

14.2.1.1.1 - NAS infrastructure Specification

Describe the TrueNAS-related configuration surface currently owned by infra/nas. The baseline is checked-in source at revision 550d7e79b1f5fdbc2b6017b75178471d6914082f, observed on 2026-09-08. The directory contains an AL configuration, DNS declarations, Vault wrappers, and brief documentation. It does not contain a TrueNAS provisioning playbook or Terraform configuration, and this baseline makes no claim about live NAS configuration or availability.

Source baseline limitation: the owning BUILD file restricts the AL target’s visibility to //infra/harvester:__subpackages__, although infra/harvester does not exist in this revision. This declaration does not establish a working consumer integration.

The project SHALL declare its AL configuration through //infra/nas:al. That configuration SHALL depend on the shared //infra:al configuration. The project’s Vault command map SHALL use its own AL target.

Sources: project description and configuration and command targets.

  • WHEN a maintainer inspects the NAS target declarations
  • THEN the AL target includes al.lua and the shared infrastructure AL dependency
  • AND the Vault command map references that NAS AL target

The NAS DNS declaration SHALL expose the NAS host and root names as A records, and SHALL distinguish the DC1 and global cloud aliases. The DC1 cloud alias SHALL target nas.alwaldend.com., while the global alias SHALL target ingress.alwaldend.com.. The DNS filegroup SHALL remain visible only within the infrastructure tree.

Sources: DNS declarations and DNS target visibility.

  • WHEN a DNS consumer selects the entries tagged dc1 and global
  • THEN it receives the NAS destination for the DC1 cloud alias and the ingress destination for the global cloud alias.

14.2.1.2 - Owned dns

14.2.1.2.1 - owned-dns Specification

Define NAS DNS management through the owner’s tf root, including canonical declarations, scoped credentials, and offline source checks before adopting live records.

The tf root SHALL consume this owner’s canonical dnsconfig.json through the shared DNS Terraform module, preserving declared record identities and views. DNS resources SHALL default to disabled until the authorized adoption revision and SHALL retain enabled ownership after existing records are imported into the owner’s state. Reconciliation against unchanged declarations and adopted state SHALL propose no record additions, changes, replacements, or deletions.

  • WHEN the checked-in root is evaluated with default inputs before its authorized adoption revision
  • THEN it reads this owner’s declaration and disables managed DNS records
  • AND the package contains the module and declaration inputs
  • WHEN the authorized adoption freezes the old central writers and enables the owner’s record management
  • THEN the owning root imports existing records by exact provider ID into its state while preserving their declared identities and views
  • WHEN the adopted root uses its checked-in source defaults
  • THEN DNS ownership remains enabled and the package retains the module and canonical declaration inputs
  • WHEN the owning root plans against its adopted state and unchanged declarations
  • THEN it proposes no record additions, changes, replacements, or deletions

The DNS wrapper SHALL select src_infra_nas through the repository AL flow and keep secret values in injected variables. DNS injection SHALL select the root’s existing Terraform stage label. Real DNS credentials and Vault policy grants SHALL be prerequisites for operational Terraform calls. RouterOS DNS credentials SHALL be isolated from unrelated RouterOS resources. The package SHALL expose a format test that does not authenticate to Vault or contact DNS providers.

  • WHEN the package format test executes
  • THEN it checks the packaged Terraform configuration without live credentials
  • AND existing non-DNS authentication and backend paths remain unchanged
  • WHEN the wrapper selects its Terraform stage labels
  • THEN it selects DNS credential injection through the owner AppRole
  • AND disabled DNS resources do not imply offline provider configuration

15 - Nexus

Planned native Nexus Repository deployment on XCP-ng

This component currently contains a proposal and its OpenSpec validation scaffolding. It contains no service implementation and establishes no deployed state. See the deployment proposal for the agreed scope and design for architecture, dependencies, and remaining deployment inputs.

infra/nexus owns the deployment-specific configuration and documentation. Its planned boundaries are tf_setup for the VM and DNS, ansible for native service deployment, and tf for Nexus API configuration. Reusable service and database behavior belongs to projects/ansible_collection; Vault identities and XO grants remain with infra/vault and infra/xcp_ng. Traefik owns ingress.

The current Bazel targets package documentation and OpenSpec sources only. The owner is registered in the repository’s OpenSpec validation suite. Implementation and live infrastructure operations require later user requests.

16 - OpenHands

openhands.alwaldend.com

OpenHands deploys as separate components because each owns a distinct responsibility and trust boundary:

Component Responsibility
Agent Canvas Browser client for conversations, files, settings, backends, automations
Agent Server Runs conversations, agents, tools, and workspace operations
Automation Server Stores schedules and triggers, tracks runs, dispatches conversations

The agent server runs directly on the host with the service account’s permissions. Its workspace is a working directory; operating-system account permissions define its access to the host. Treat the agent server host as trusted infrastructure.

  • tf_setup creates the Agent Canvas, secured agent server, and automation server VMs in the src_infra_openhands Xen Orchestra resource set.
  • ansible deploys the three VM components. The host-bot project owns and deploys its unsecured agent server.
  • dnsconfig.json owns the component hostnames and static addresses.
  • al.lua authenticates with the src_infra_openhands Vault AppRole, signs host keys for Ansible, and injects the component secrets.

There is no tf package: these components keep their state in files and SQLite on their own hosts, so no API-side resources exist to declare.

Canvas serves the packaged browser assets through its static-server service on loopback. Configure the browser’s backend with the canvas origin as its host and the agent server session key as its credential.

Each component host runs Traefik from the shared role with certificates issued by src_infra_openhands_pki_server. The canvas origin proxies /api/*, /api/automation, /sockets, and /server_info to the agent server and automation server, so the browser talks to one origin and issues no cross-origin request.

The public canvas.openhands.alwaldend.com record is a CNAME to ingress.alwaldend.com, and infra/ingress forwards it to dc1.canvas.openhands.alwaldend.com. That ingress keeps its RequireAndVerifyClientCert client-auth policy, so reaching canvas from outside still requires a client certificate.

agent_server holds session_api_key and secret_key; automation holds kv_secret. The session API key is shared by the agent server, the automation server, and the browser because all three authenticate the same X-Session-API-Key value.

The unsecured host-bot server has a separate secret_key reference owned by its host’s AL configuration.

Prepare one private JSON input for each path through the secret-handling workflow. Set OPENHANDS_AGENT_SERVER_JSON, OPENHANDS_AUTOMATION_JSON, and OPENHANDS_HOST_BOT_JSON to those input file paths. Each JSON object contains the fields listed above for its path. Initialize the paths through standard input; -cas=0 prevents overwriting existing data:

bazel_agent bazel run //infra/openhands:vault.kv_put -- -cas=0 \
  alwaldend.com/vault1/approles/src_infra_openhands/agent_server \
  - < "$OPENHANDS_AGENT_SERVER_JSON"
bazel_agent bazel run //infra/openhands:vault.kv_put -- -cas=0 \
  alwaldend.com/vault1/approles/src_infra_openhands/automation \
  - < "$OPENHANDS_AUTOMATION_JSON"
bazel_agent bazel run //users/simeonwarren:vault.kv_put -- -cas=0 \
  alwaldend.com/vault1/approles/user_simeonwarren/openhands \
  - < "$OPENHANDS_HOST_BOT_JSON"
bazel run //infra/openhands/tf_setup:tf_setup.plan
bazel run //infra/openhands/tf_setup:tf_setup.apply
bazel run //infra/openhands/ansible

The component AppRole must first exist in Vault. Follow the XO authentication and resource-set workflow to synchronize its OIDC identity and apply its resource-set assignment before tf_setup runs. The assignment in resource_set_inventory supplies the template, storage, and network by name. Component secrets must be written to Vault before Ansible runs. State and locking use the repository’s tf_backend plugin, which stores them as Vault KV entries and creates the entries on first use, so no separate state bucket is required.

16.1 - Ansible

OpenHands deployment

Deploys the OpenHands components. The inventory has three hosts in three groups, and the playbook applies one component role per group:

Group Host Component
openhands_canvas host1.canvas.openhands.alwaldend.com Agent Canvas
openhands_server_secure host1.server.openhands.alwaldend.com Agent server (secured)
openhands_automation host1.automation.openhands.alwaldend.com Automation server

The three XCP-ng VMs exist only for the secured agent server and for the components that must not share a trust boundary with host-bot. The secured group sets openhands_server_session_api_key and hands the same value to the automation server so the automation dispatcher can authenticate its dispatched conversations.

Every component applies host and traefik. Canvas runs the published package’s static server directly on loopback. The operator configures the browser backend with the canvas origin and the shared session key.

The canvas host routes /api/automation to the automation host and the agent server API prefixes to the secured agent server. These routes take precedence over the frontend route, so the configured browser reaches one origin. Downstream routers accept the original canvas Host header.

The unsecured agent server runs on host-bot and is deployed by the host-bot project, which owns that host entirely. It omits OH_SESSION_API_KEYS_0 and binds to loopback without a session API key. Access is local to host-bot; its Traefik and firewall configuration add no route for this server.

The group variables read the session key, secret key, and automation KV secret from the environment the AL Vault injector populates.

16.2 - OpenSpec

16.2.1 - owned-dns Specification

Define OpenHands DNS management through the owner’s tf_setup root, including canonical declarations, scoped credentials, and offline source checks before adopting live records.

The tf_setup root SHALL consume this owner’s canonical dnsconfig.json through the shared DNS Terraform module, preserving declared record identities and views. DNS resources SHALL default to disabled before adoption and SHALL default to enabled in the reviewed adoption revision. Adoption SHALL bind existing records to their exact provider identities and SHALL preserve their declared attributes. Existing-record adoption SHALL contain no DNS record additions, changes, replacements, or deletions. Verified missing declarations MAY be provisioned through an exact additions-only DNS plan that preserves every existing record and excludes non-DNS managed-resource changes. After deployment, default DNS reconciliation SHALL retain the bindings and preserve unrelated provider records.

  • WHEN the checked-in root is evaluated with default inputs before adoption
  • THEN it reads this owner’s declaration and disables managed DNS records
  • AND the package contains the module and declaration inputs
  • WHEN the reviewed adoption revision is evaluated with default inputs after adoption
  • THEN it reads this owner’s declaration and enables managed DNS records
  • AND the package contains the module and declaration inputs
  • WHEN the owner’s existing records are adopted through its scoped DNS wrappers
  • THEN Terraform binds the records to their exact provider identities
  • AND their declared values, attributes, and views remain unchanged
  • AND the reviewed adoption plan contains no DNS additions, changes, replacements, or deletions
  • WHEN the adopted root plans with default inputs and unchanged declarations and provider records
  • THEN the plan retains the adopted bindings and contains no DNS additions, changes, replacements, or deletions
  • AND unrelated provider records remain unchanged
  • WHEN fresh complete provider inventories prove the owner’s declared records are absent and conflict-free
  • THEN the reviewed DNS plan creates only those declared missing records
  • AND existing provider identities and attributes remain unchanged
  • AND a subsequent DNS plan contains no changes

The dns.plan, dns.show, and dns.apply wrappers SHALL select dns=1 through the repository AL flow using src_infra_openhands, and operate on module.dns and its dependencies in the existing tf_setup root and Vault HTTP backend. Apply SHALL consume the reviewed saved plan. Ordinary VM setup SHALL retain its tf=setup and xoa_login=1 labels, XO authentication, and existing backend paths. Secret values SHALL remain in injected variables. Real DNS credentials and Vault policy grants SHALL be prerequisites for operational DNS calls. RouterOS DNS credentials SHALL remain isolated from unrelated RouterOS resources. Successful scoped DNS execution SHALL not establish VM or service health. The package SHALL expose a format test that does not authenticate to Vault or contact DNS providers.

  • WHEN the package format test executes
  • THEN it checks the packaged Terraform configuration without live credentials
  • AND existing non-DNS authentication and backend paths remain unchanged
  • WHEN the scoped DNS wrapper selects dns=1
  • THEN it selects DNS credential injection through the owner AppRole and existing backend without starting XO login
  • AND the targeted plan and saved-plan apply cover DNS and its dependencies, without establishing VM or service health
  • AND disabled DNS resources do not imply offline provider configuration

16.3 - Tf setup

Terraform setup

This package creates Xen Orchestra VMs in the src_infra_openhands resource set for the three OpenHands components that run off host-bot: Agent Canvas, the agent server, and the automation server. infra/openhands/al.lua authenticates with OpenHands’ own Vault AppRole and the packaged XO OIDC login plugin. The plugin supplies a temporary XO token and revokes it on shutdown; no infrastructure administrator token is loaded. The setup HTTP backend remains owned by the same OpenHands config.

Before running VM setup, bootstrap the AppRole’s XO OIDC user and apply its resource-set membership through infra/xcp_ng/tf. Subjects are matched by immutable Vault entity UUID under the configured OIDC issuer, not by login name or AppRole group membership. See XO authentication.

vms holds the component definitions, and each entry names the dnsconfig record prefix that owns its hostname and address. Adding a component means adding a record to ../dnsconfig.json and an entry here; the guest hostname, static address, and derived MAC all come from that one record. The agent server is sized larger because it stores every backend conversation, workspace, and bash event.

Defaults select the imported Fedora 44 template, local storage and wired network by name from the OpenHands resource set. Override TF_VAR_xoa_template_name, TF_VAR_xoa_storage_name, and TF_VAR_xoa_network_name for another assignment. Native provider lookups run as OpenHands’ own identity and reject ambiguous matches. The selected network must carry 192.168.10.0/24; gateway and DNS default to 192.168.10.1 (the wired router) and can be overridden.

The PVE-based infra/cloud_init:xen_linux target supplies the shared Ansible user and CA configuration through the repository template rule. Terraform adds the hostname and static network configuration from ../dnsconfig.json. After cloud-init completes, verify that /dev/xvda is the boot disk before running Ansible.

bazel_agent bazel run //infra/openhands/tf_setup:tf_setup.plan
bazel_agent bazel run //infra/openhands/tf_setup:tf_setup.apply

Use this package’s dns.plan, dns.show, and dns.apply targets for the scoped DNS adoption workflow. They select dns=1 and target module.dns in the same root, AppRole, and backend without starting XO login. Apply requires the reviewed saved plan; the shared procedure owns preparation and reconciliation.

dns_enabled defaults to true after verified initial provisioning. Keep it enabled to retain the managed records. Unchanged declarations and provider records produce a no-change DNS plan. Successful DNS checks do not establish VM, OpenHands service, or TLS readiness; the separate service deployment change retains that acceptance work.

17 - Pve

Proxmox cluster pve.alwaldend.com

This archived diagram preserves the historical source topology. The Mermaid source is authoritative; Bazel renders its light and dark SVGs.

Archive/Proxmox architectureArchive/Proxmox architecture

bazel run //infra/pve/ansible # host setup
bazel run //infra/pve/tf/tf.apply # tf setup
bazel run //infra/pve/tf:tf.apply -- --replace module.vm_cloudinit_test.proxmox_vm_qemu.vm
bazel run //infra/pve/ansible -- --tags pve_snippets
  • Create an EAB:
    bazel run //infra/dc1/vault -- write -f pki/ica_servers/roles/src_infra_dc1_pve1_pki_server/acme/new-eab
    
  • Login as root@pam
  • Go to Datacenter -> ACME
  • Create a new account with the EAB and a directory:
    https://vault.dc1.alwaldend.com:8200/v1/pki/ica_servers/roles/src_infra_dc1_pve1_pki_server/acme/directory
    
  • Create a token with Privilege Separation
  • Grant it required roles
  • Create json:
    {
      "token_id": "",
      "token_secret": ""
    }
    
  • Write the data:
    bazel run //infra/pve:vault.kv_put alwaldend.com/vault1/approles/src_infra_dc1_pve1/pve_token @"${PWD}/data.json"
    

17.1 - Ansible

Ansible setup

17.2 - OpenSpec

17.2.1 - Specifications

17.2.1.1 - Infra pve

17.2.1.1.1 - Proxmox infrastructure Specification

Specify the Proxmox cluster configuration, resource pools, and bootstrap test VM owned by infra/pve. The baseline is checked-in source at revision 550d7e79b1f5fdbc2b6017b75178471d6914082f, observed on 2026-09-08. It describes declared infrastructure and packaged operator entry points; deployment and cluster health have not been observed for this baseline.

The Terraform configuration SHALL resolve the members of Vault’s approles identity group and declare one Proxmox pool per resolved entity, keyed and named by that entity’s name. It SHALL also declare a separate templates pool.

Sources: project documentation and resource pool definitions.

  • WHEN Terraform resolves the entities in the Vault approles group
  • THEN the desired configuration includes a pool with each entity’s name and the dedicated templates pool.

The PVE Ansible entry point SHALL package the inventory and deployment playbook and apply the shared host and PVE roles to the pve1 inventory group. It SHALL package the canonical shared cloud-init files under files/cloud_init.yaml and files/cloud_init_min.yaml, preserving the existing snippet update flow.

Sources: deployment target, playbook, and snippet variables.

  • WHEN the Ansible binary’s source package is built
  • THEN it includes the playbook, inventory, shared role collection, and both shared cloud-init files under their established aliases.

The Terraform configuration SHALL declare its cloud-init test VM through the shared pve_vm_qemu module, derive its hostname and address from the project’s DNS configuration, and place it in the src_infra_dc1_pve1 AppRole pool. The baseline declaration SHALL use VM ID 100, a 5G scsi0 disk, and the test tag.

Sources: VM definition and DNS configuration.

  • WHEN Terraform evaluates module.vm_cloudinit_test
  • THEN its name and /24 address derive from cloudinit_test DNS fields, and its pool resolves to the declared PVE AppRole pool.

PVE Terraform commands SHALL use the project’s AL configuration and the declared Terraform, PVE-login, and Vault-environment plugins. The package SHALL include the lockfile, Terraform source, shared backend and VM modules, and required Vault helper targets. PVE Ansible commands SHALL likewise use the project AL configuration and packaged injector.

Sources: Terraform wrapper and Ansible wrapper.

  • WHEN a repository Terraform command target is invoked with authorization for its operation
  • THEN the wrapper supplies the declared modules and helper inputs and selects the tf, pve_login, and vault_env=default_default plugins.

17.2.1.2 - Owned dns

17.2.1.2.1 - owned-dns Specification

Define Proxmox cluster DNS management through the owner’s tf root, including canonical declarations, scoped credentials, and offline source checks before adopting live records.

The tf root SHALL consume this owner’s canonical dnsconfig.json through the shared DNS Terraform module, preserving declared record identities and views. DNS resources SHALL default to disabled until the authorized adoption revision and SHALL retain enabled ownership after existing records are imported into the owner’s state. Reconciliation against unchanged declarations and adopted state SHALL propose no record additions, changes, replacements, or deletions.

  • WHEN the checked-in root is evaluated with default inputs before its authorized adoption revision
  • THEN it reads this owner’s declaration and disables managed DNS records
  • AND the package contains the module and declaration inputs
  • WHEN the authorized adoption audits competing writers, keeps identified writers stopped within the recorded coverage, and enables owner record management
  • THEN the owning root imports existing records by exact provider ID into its state while preserving their declared identities and views
  • WHEN the adopted root uses its checked-in source defaults
  • THEN DNS ownership remains enabled and the package retains the module and canonical declaration inputs
  • WHEN the owning root plans against its adopted state and unchanged declarations
  • THEN it proposes no record additions, changes, replacements, or deletions

The DNS wrappers SHALL select src_infra_dc1_pve1 through the repository AL flow and keep secret values in injected variables. dns.plan, dns.show, and dns.apply SHALL select dns=1 and operate on module.dns in the existing tf root and backend. Apply SHALL consume the reviewed saved plan. Required root inputs SHALL retain their existing Vault readers, while ordinary Proxmox authentication SHALL remain separate. Scoped DNS operations SHALL verify DNS and its dependencies without establishing overall PVE service health. Real DNS credentials and Vault policy grants SHALL be prerequisites for operational Terraform calls. RouterOS DNS credentials SHALL be isolated from unrelated RouterOS resources. The package SHALL expose a format test that does not authenticate to Vault or contact DNS providers.

  • WHEN the package format test executes
  • THEN it checks the packaged Terraform configuration without live credentials
  • AND existing non-DNS authentication and backend paths remain unchanged
  • WHEN the scoped DNS wrapper selects dns=1
  • THEN it selects DNS credential injection through the owner AppRole and uses the existing root and backend without starting Proxmox login
  • AND the targeted plan and saved-plan apply cover DNS and its dependencies, without proving PVE host or API health
  • AND disabled DNS resources do not imply offline provider configuration

17.3 - Tf

Terraform config

Use this package’s dns.plan, dns.show, and dns.apply targets for the scoped DNS adoption workflow. They select dns=1 and target module.dns in this root, retaining the src_infra_dc1_pve1 AppRole and existing backend. Use the owner preparation and import procedure for declarative import maps and reviewed saved-plan apply.

dns_enabled defaults to true after the completed adoption. Reconciliation against adopted state and unchanged declarations produces no DNS changes. DNS targeting covers DNS and its dependencies; it does not establish PVE host, API, or service health. Ordinary Terraform commands retain their existing Proxmox authentication.

18 - Repositories

Shared organization, repository, and access configuration

This project owns the organization and repository catalog consumed by GitHub, GitLab, and Forgejo. Named organization administrators and developers belong in this catalog. Root configuration owns the schema version and defaults. Each organization owns orgs/<organization>/org.json and one orgs/<organization>/repos/<repository>.json file per repository. Vault continues to own Forgejo login identities and service-specific access.

First-party means owned by us, regardless of forge. Our repositories retain their chosen names across GitHub, GitLab, and Forgejo: alwaldend/src stays alwaldend/src, including when copied or synchronized between those forges.

Forks and mirrors of external repositories must belong to an organization in the catalog. Their names use the original upstream hostname in reverse order, followed by its full owner and repository path, in lowercase with punctuation replaced by underscores. For example, https://gitlab.com/fdroid/fdroiddata becomes alwaldend/com_gitlab_fdroid_fdroiddata. Copies on another forge keep that name; they do not add a prefix for our intermediate copy. This naming rule also applies to external repositories imported once for later syncing.

The Terraform module owns this derivation. A forge key opts a repository into that consumer. GitLab import_from refers to the corresponding catalog source; fork_from identifies a GitLab upstream. External origins belong in the repository’s upstream_url. Forge-specific overrides preserve settings such as Pages sites and repository descriptions during adoption.

Consumers retain their own providers, state, and Vault authentication. They adopt existing remote resources by import and preserve managed resource addresses or declare explicit moves. Named developers can read repositories, write feature branches, and open pull or merge requests; protected default branches restrict pushes and merges to administrators and any separately owned existing service grants. Catalog GitHub repositories permit merge commits only: the shared defaults disable squash merges and rebase merges, and a catalog precondition rejects any repository record that enables them. Merging therefore keeps the reviewed feature commit as a parent of the default-branch commit instead of replacing it. GitLab and Forgejo merge methods are not owned here. Dedicated per-project landing repositories are retired: the main site publishes every project landing at /projects/<name>/, so the catalog carries no landing repository. The apex site repository keeps the Pages configuration that serves it.

Catalog targets are repository-internal infrastructure inputs. They are not production build dependencies or published artifacts.

GitLab receives one-time repository imports and the configured upstream fork. The user will arrange ongoing synchronization separately; the decision and adoption evidence live in OpenSpec.

18.1 - OpenSpec

18.1.1 - repository-catalog Specification

Define shared organization, repository identity, naming, and named access configuration for the GitHub, GitLab, and Forgejo infrastructure consumers.

The repository catalog SHALL store shared defaults and schema version at its root, one configuration file per organization, and one configuration file per repository beneath its organization. Those records SHALL own the configured organizations, repositories, named administrators, and named developers. Each forge consumer SHALL use that catalog for its selected repositories and named roles, preserving forge-specific settings without maintaining another copy of the inventory. Authentication and service-specific grants SHALL remain with their existing owners.

  • WHEN an organization assigns named administrators and developers and selects repositories for multiple forges
  • THEN each selected consumer uses the same catalog assignments and repository identities
  • AND forge-specific settings and service identities remain with their declared owners

First-party repository names SHALL remain the same across forges, including when copied or synchronized. External forks and mirrors SHALL belong to a catalog organization and use a name derived from the original upstream’s reversed hostname followed by its full owner and repository path, in lowercase with punctuation replaced by underscores. Cross-forge copies of external repositories SHALL retain that name rather than naming the intermediate copy as their upstream.

  • WHEN the first-party alwaldend/src repository is copied from GitHub to GitLab or Forgejo
  • THEN its destination remains alwaldend/src
  • WHEN https://gitlab.com/fdroid/fdroiddata is forked into alwaldend
  • THEN its organization-owned name is alwaldend/com_gitlab_fdroid_fdroiddata
  • AND subsequent copies on another forge retain that name

Consumers SHALL grant catalog administrators administrative access and catalog developers repository reads, feature-branch writes, and pull or merge request creation. Catalog developers SHALL NOT receive direct push or merge access to protected default branches through their named roles or overlapping retained grants. Existing service-specific permissions SHALL be preserved and distinguished from named developer assignments.

  • WHEN a catalog developer accesses a selected repository
  • THEN the developer can read its contents, create a feature branch, and open a pull or merge request
  • AND the developer cannot push or merge to its protected default branch

Consumers SHALL adopt existing remote resources through imports and preserve their remote identities. Existing managed resource addresses SHALL remain stable or use explicit state-preserving moves. Existing GitHub repository settings and Pages configuration SHALL remain intact, and default branches SHALL remain unchanged. No deletion or replacement SHALL execute without separate user approval for its concrete scope.

  • WHEN a catalog repository already exists remotely or in Terraform state
  • THEN its import or state move preserves the existing remote repository ID and contents
  • AND the reviewed plan does not recreate it to resolve a name collision
  • WHEN any reviewed plan proposes a deletion or replacement
  • THEN execution of that operation stops pending the user’s explicit approval for the identified operation and scope

GitLab SHALL receive a one-time Git import of every catalog-selected GitHub repository and an organization-owned fork of the selected F-Droid metadata upstream. Imports SHALL preserve the source default branch, and the fork SHALL retain its upstream fork relationship. This change SHALL configure no ongoing synchronization.

  • WHEN the GitHub default-branch migration completes and the initial GitLab imports and fork finish
  • THEN every selected GitHub repository has its intended GitLab copy and source default branch
  • AND the metadata repository retains its F-Droid upstream fork identity
  • AND later upstream changes are not promised to synchronize

GitLab default-branch access SHALL be established through group defaults before new repositories are populated. Existing automatically created protections SHALL be verified and imported before standalone protection management is applied. Adoption SHALL preserve the existing protections; provider limitations SHALL be reported rather than bypassed by unprotecting or recreating them.

  • WHEN an imported GitLab repository has an existing default-branch rule
  • THEN Terraform adopts that rule by import before applying permitted in-place access changes
  • AND adoption does not invoke unprotect-and-recreate behavior

18.2 - Repository catalog module

This provider-free Terraform module reads the catalog files, merges forge defaults, and projects organization-owned repository names and settings for each consumer. It creates no resources and uses no credentials.

Fork and mirror names follow the catalog naming contract. Output keys retain the organization and catalog key, independently of the derived destination name. The consumers retain their existing state addresses where resources were already managed.

The catalog test runs without network access or provider initialization:

bazel_agent bazel test //infra/repos/tf:tf_tests.catalog_test

19 - Source repository

Repository evolution specifications and workflow history

This project owns the OpenSpec workspace describing evolution of the alwaldend/src monorepo: its shared structure, build system and development workflows. It also retains the index of the goal-to-OpenSpec migration. Individual projects own their own specifications and change history.

The repository root remains the Bazel workspace and owns shared executable configuration. This directory owns repository development specifications; adding or changing them does not provision infrastructure.

Run the pinned CLI from the repository root. It selects this project by default:

bazel_agent bazel run //tools/openspec -- list --specs

19.1 - Repository evolution

This OpenSpec workspace describes evolution of the alwaldend/src repository itself: its shared structure, build system and development workflows. specs/repository/spec.md records that contract; changes/ records proposed and completed changes to it.

Each component owns a separate OpenSpec workspace beside its source:

  • projects/<project>/openspec/ holds that project’s specs and changes.
  • tools/<project>/openspec/ holds that tool project’s specs and changes.
  • infra/<project>/openspec/ holds that infrastructure project’s specs and changes.
  • infra/src/openspec/ holds the repository’s own specs and changes.

Select the narrowest owner of the requested behavior. A project change belongs in that project’s workspace, including when it has a standalone Bazel module. Changes to shared repository structure belong here. Work spanning owners keeps each affected contract with its owner and links related changes. Component READMEs and build declarations retain their existing authority.

The pinned CLI runs from the Git worktree root. It selects infra/src by default; use OPENSPEC_PROJECT for a component:

bazel_agent bazel run //tools/openspec -- list --specs
OPENSPEC_PROJECT=tools/agents bazel_agent bazel run //tools/openspec -- list
OPENSPEC_PROJECT=infra/vault bazel_agent bazel run //tools/openspec -- list --specs

Use new change, status, instructions, validate, and archive with the same owner selection. The packaged Repository OpenSpec skill documents continuation and acceptance. No global install or assistant configuration rewrite is needed. The CLI isolates configuration and disables telemetry.

Repository validation checks every component workspace as well as this one:

bazel_agent bazel test //infra/src/openspec/validation:validate_test //infra/src/openspec/validation:archive_test

Validation checks artifact structure and, for archives, task completion. Requirement acceptance still needs evidence from the owning implementation. Infrastructure specs describe checked-in definitions, not observed deployment.

In the selected workspace, changes/<name>/proposal.md states outcome and scope, design.md records decisions and continuation, tasks.md records remaining actions, and specs/<capability>/spec.md contains requirement deltas. Read an existing matching change before creating another. Keep temporary notes, raw logs and validation receipts under ignored out/<task>/.

On interruption, preserve the candidate, evidence, execution state and next action. Keep blocked or unfinished work explicit. OpenSpec files and Git do not supply the removed legacy goal store’s transaction locks or evidence verdicts; one coordinator owns a change and reconciles concurrent edits.

After acceptance, validate and archive the change in its owning workspace, then inspect the resulting baseline specs. Specifications and generated instructions neither expand user authority nor introduce an approval gate.

migration.md maps the nine maintained goals to their current owner-local changes. It records a repository workflow migration; the actual history and work state live with Agents, MCP Cordis and Renders. Original history, checksums and acceptance states are preserved. Historical deltas were archived without applying them to current specs. Reimu Fumo remains open and blocked.

The legacy goal tool, its skill, and its store have been removed. New maintained work uses the owning OpenSpec workspace; the migration record above preserves the historical record.

19.1.1 - Goal migration to OpenSpec

On 2026-09-08, all nine tracked maintained project goal records at source commit 550d7e79b1f5fdbc2b6017b75178471d6914082f were migrated into native OpenSpec changes. Each change now lives in its owning project’s openspec/ workspace. Eight completed records are imported as archived history. The Reimu Fumo change remains open with blocked execution and no accepted candidate.

The machine-readable inventory maps all 222 tracked source files (834,320 bytes) to byte-identical provenance snapshots. Test fixtures and ignored task scratch are outside the maintained-record inventory.

Former maintained goal OpenSpec change Preserved outcome Preserved execution
projects/agents/goals/agent-system-phase-1 agent-system-phase-1 achieved paused
projects/agents/goals/agent-system-phase-2 agent-system-phase-2 achieved paused
projects/agents/goals/agent-system-phase-3 agent-system-phase-3 achieved paused
projects/agents/goals/agent-system-phase-4 agent-system-phase-4 achieved paused
projects/agents/goals/agent-system-phase-five-followup agent-system-phase-five-followup achieved paused
projects/agents/goals/agent-system-phase-six-ergonomics agent-system-phase-six-ergonomics achieved paused
projects/agents/goals/repo-agent-system repo-agent-system achieved paused
projects/renders/goals/reimu-fumo-finish reimu-fumo-finish open blocked
projects/mcp_cordis/goals/runtime_extensions runtime_extensions achieved unavailable in source
  • Objective and scope map to proposal.md; accepted plans, decisions, rejected approaches, and constraints map to design.md.
  • The latest acceptance criteria map to tasks.md and meaningful capability delta requirements. Each YAML criterion keeps its stable ID and revision in migration.json; original revisions, attempts, review verdicts, and evidence remain in provenance.
  • Completed task checkboxes reproduce explicit historical acceptance reviews. They do not revalidate today’s source. The Markdown Cordis record declares Complete and no failing criteria; its absent execution field stays unavailable rather than being guessed.
  • Reimu acceptance and continuation tasks remain unchecked. A supported authorized unblock can change execution while the outcome remains open until every required criterion passes.
  • Completed records were placed directly in their owner’s openspec/changes/archive/2026-09-08-<id>/. No archive or spec-sync command applied their historical deltas. Current baseline specifications were derived separately from maintained owner source. Do not reapply historical deltas.
  • Historical Phase 5 acceptance includes an explicit fixture-gap inventory. Its accepted review and still-pending prose are both retained; migration does not claim the missing infrastructure fixtures were implemented.

Each change has migration.json, a checksum inventory at provenance/manifest.json, original record bytes at provenance/source/, and a provenance navigation guide. The old agent goal landing and BUILD file are also retained under the repository-agent-system archive. The historical BUILD snapshot uses .original to preserve bytes without creating a new Bazel package.

Among preserved Markdown links, 47 links depend on the former location; companion navigation tables resolve them where the referenced artifact exists. 46 historical targets are unavailable in the checkout. Original bytes and historical Git identities are not rewritten to disguise unavailable scratch or superseded paths.

The former maintained goal directories are removed, and the legacy goal tool and its store were later removed as well; native OpenSpec work is the continuation surface. Provenance files are excluded from generic formatting so their source digests remain stable. The migration inventory is import evidence, not an alternate work scheduler or mutable status catalog. The removal change is remove-deprecated-goal-component.

19.1.2 - Specifications

19.1.2.1 - Go mod version

19.1.2.1.1 - go-mod-version Specification

Keep tracked go.mod files on the Go version configured for tools/go_mod.

Sources: tool README, tool BUILD, and archive.

The repository SHALL provide a tools/go_mod command that discovers every tracked go.mod file and sets its go directive to the version configured in the tool’s BUILD file. The command MUST offer an update mode and a check mode.

  • WHEN a user runs the update target
  • THEN every tracked go.mod file whose go directive differs from the configured version is rewritten to that version
  • AND files already on the configured version remain unchanged
  • WHEN the check test runs
  • THEN it fails if any tracked go.mod file has a go directive different from the configured version
  • AND it passes when every tracked go.mod file matches

The go.mod version check MUST run as part of //:repo_quality_test so a version mismatch fails repository quality.

  • WHEN a go.mod file’s go directive differs from the configured version
  • THEN //:repo_quality_test fails

19.1.2.2 - Python import validation

19.1.2.2.1 - python-import-validation Specification

Enforce module namespace imports in tracked first-party Python source through repository quality.

The repository quality suite SHALL reject individual symbol imports in tracked first-party Python source using the existing pinned syntax linter, except direct public re-exports listed in explicit __all__ in __init__.py. Wildcard imports SHALL remain forbidden. Module imports and qualified member access SHALL be accepted, including importing a verified module with from package.path import module. Long module paths SHALL use this form. Downloaded external source SHALL remain outside this first-party check. The Python skill SHALL describe the convention and its owning validation target.

  • WHEN tracked Python imports individual members outside explicit package re-exports or uses a star from-import
  • THEN repository quality fails with a namespaced-import diagnostic
  • WHEN source imports modules, including verified module from-imports, or includes from-import text only in comments or strings
  • THEN the import check passes
  • WHEN existing first-party imports are migrated to the enforced convention
  • THEN their module references and supported package exports remain usable
  • WHEN __init__.py imports direct symbols and lists their bound names in explicit __all__
  • THEN the import check accepts those exports, including relative imports

19.1.2.3 - Repository

19.1.2.3.1 - Repository Evolution Specification

Define how the alwaldend/src monorepo evolves its shared structure, build system and development workflows. Component behavior is specified in each owner’s local OpenSpec workspace. This baseline was inspected at source revision 550d7e79b1f5fdbc2b6017b75178471d6914082f on 2026-09-08 and incorporates the OpenSpec migration delivered with this specification. Changes in infra/src record repository evolution; they do not collect unrelated component work or claim runtime health or deployed infrastructure.

Sources: repository map, agent policy, root build, project boundary, infrastructure boundary, tool boundary, and OpenSpec workflow.

The repository MUST keep component purpose and publication boundaries in the nearest owner README, executable and dependency structure in BUILD and MODULE files, and agent policy in the applicable AGENTS.md chain. Specifications SHALL describe component requirements with links to these owners.

  • WHEN a change affects a project, tool or infrastructure component
  • THEN its owning documents and Bazel workspace determine the affected paths and supported consumers
  • AND a repository-wide catalog or specification does not grant broader publication or execution authority

Repository agent build and test operations MUST use bazel_agent bazel in the owning workspace with pinned dependencies and the repository’s shared agent configuration. Generated dependency and catalog files MUST be updated through their owning generator.

Every nested module that declares MODULE.bazel MUST declare only dependencies its own sources use, with a resolvable version or an override that applies when that module is the root. Every nested module directory MUST appear in the root .bazelignore so root target expansion does not cross the workspace boundary. Each nested workspace MUST build and test standalone.

  • WHEN an implementation adds an external build input
  • THEN its owner records an immutable version and integrity information
  • AND applicable generated locks and package checks validate the declared dependency through the pinned build workflow
  • WHEN a nested Bazel workspace is built or tested on its own
  • THEN its module graph resolves without depending on root-only overrides
  • AND the workspace builds and tests through its shared configuration
  • WHEN the root workspace expands targets beneath a nested module directory
  • THEN the nested workspace is excluded by the root ignore list
  • AND root expansion neither loads nor silently omits that module

Repository modifications MUST use a dedicated feature branch and linked worktree. Task scratch MUST remain under ignored out/<task>/ in the applicable workspace. Acceptance SHALL identify the candidate and relevant checks and representative output before delivery.

  • WHEN an authorized implementation is ready for delivery
  • THEN task-owned changes pass applicable formatting, quality and semantic checks and are committed and published through the repository procedure
  • AND unrelated edits, scratch and protected data are excluded

Persistent infrastructure configuration MUST be defined in checked-in owning infrastructure code. Infrastructure operations MUST require the user’s exact operation and scope authorization; an implementation validation request SHALL NOT authorize live deployment.

  • WHEN an agent adds or updates an infra component specification
  • THEN it inspects source and runs applicable offline checks
  • AND it does not infer deployed state or execute live apply or deployment from the existence of the specification

Each direct project in projects/ and infra/ SHALL keep its baseline specifications and maintained changes in its own openspec/ workspace. infra/src/openspec SHALL describe evolution of the repository itself, including shared structure, build system and development workflows. The repository root SHALL NOT collect component specifications or change records. Every repository change MUST use an OpenSpec change in its owning workspace, including trivial edits, documentation, configuration, and dependency changes. A matching active change SHALL be reused when available, and artifacts SHALL remain proportional to scope. Read-only questions and reviews do not require a change record. Maintained work SHALL preserve outcome, decisions, tasks and requirement deltas. Existing task authority MUST survive workflow transitions.

  • WHEN a change affects a project’s supported behavior
  • THEN its specifications and change artifacts remain in that project’s openspec/ directory
  • AND the pinned CLI and context routing select that owner workspace
  • WHEN a change affects the monorepo’s shared structure or development workflow
  • THEN infra/src/openspec records its repository requirements and evolution
  • AND component contracts remain with their respective owners
  • WHEN an agent resumes a named OpenSpec change
  • THEN it reads that owner’s change artifacts, candidate, evidence and next action
  • AND unfinished or blocked acceptance remains explicit until evidence supports completion
  • WHEN a project has its own Bazel module
  • THEN repository OpenSpec validation still checks its declared local workspace
  • AND a successful repository check does not omit that project’s requirements
  • WHEN an agent makes a one-line repository correction
  • THEN it selects or creates a matching owner-local OpenSpec change before editing, with small artifacts and skip_specs if behavior is unchanged
  • AND the record adds no approval gate to already authorized work
  • WHEN the task only inspects a bounded change set and reports findings
  • THEN no new OpenSpec change is required for that review
  • AND the review itself does not authorize implementation or publication

The migration SHALL preserve all tracked maintained goal history and its recorded acceptance and execution states with source-to-change mappings and checksums. The goal skill MUST be disabled for discovery and the goal tool MUST identify itself as deprecated compatibility functionality.

  • WHEN a reader follows a legacy goal’s migration entry
  • THEN its OpenSpec change exposes the original history and status
  • AND historical completion does not establish acceptance for a different candidate or automatically apply old specification deltas

Bazel rules MUST NOT embed templated executable content in rule source. A rule that needs an executable MUST reference an ordinary checked-in script and supply its inputs as declared arguments or data files. Every public rule set MUST generate stardoc documentation for its public .bzl entry points, and the repository’s main documentation output MUST include that generated documentation.

  • WHEN an implementation needs a rule to run a script
  • THEN the script is checked in as a normal file with its own target and receives data through arguments or a declared data file
  • AND generated shell or program text does not live in the .bzl source
  • WHEN a project publishes a public Bazel rule or macro
  • THEN it declares stardoc coverage for the declaring file
  • AND the generated pages appear in the repository documentation build

First-party standalone Bazel rule modules SHALL live under tools/<module>/. Their relocation MUST preserve module names, supported public rule and macro interfaces, standalone workspace resolution, generated rule documentation, and owner-local specifications. Repository consumers and discovery mechanisms MUST resolve their new source locations through the owning declarations.

  • WHEN a root or nested workspace loads a relocated rule module through its supported module name and public rule interface
  • THEN the module resolves from its new tool-owned location
  • AND its supported build behavior remains available without changing the public module identity
  • WHEN repository documentation, skill discovery, or OpenSpec validation traverses the relocated modules
  • THEN it selects the modules’ new owner paths
  • AND generated rule documentation and existing component specifications remain included in their declared outputs and checks

The relocated rule modules SHALL have no dedicated project landing-site configuration. After authorized retirement, their selected landing repository and DNS declarations MUST be absent from the owning infrastructure, and shared-catalog consumers MUST exclude those retired repositories. Retirement MUST preserve unrelated repositories, sites, access grants, and authentication resources.

  • WHEN the catalog is projected for GitHub or GitLab after rule-landing retirement
  • THEN the retired landing repositories are absent
  • AND remaining repositories retain their catalog identities, names, and configured access behavior
  • WHEN the owning declarative workflows execute the user’s explicitly authorized retirement scope
  • THEN the selected Pages repository and matching DNS record are removed
  • AND the reviewed plans and postconditions show no unrelated deletion or replacement

Repository CI workflows MUST be maximally thin wrappers. Executable logic SHALL be implemented in bazel run targets or committed scripts; workflow YAML SHALL contain only declarative orchestration and short command invocations. The discoverable repo-ci skill SHALL document this rule and link owning sources.

  • WHEN a workflow requires resource checks, authentication, parsing, loops, or other executable behavior
  • THEN that behavior resides in a committed implementation invoked by the workflow, preferably through bazel run
  • AND inline programs, heredocs, and composite-action YAML do not substitute for the implementation
  • WHEN an agent discovers the repository skills for CI work
  • THEN repo-ci explains the workflow and command boundaries, authentication constraints, and applicable offline and live validation entry points

Requirement: Shared repository CI entry point

Every repository CI workflow SHALL use the local tools/ci/action action after checkout. The action SHALL launch bazel run --config=ci //tools/ci without a shell and propagate its failure. The entry point SHALL build and test all normal targets in the root and nested Bazel workspaces, continuing to attempt both phases and remaining workspaces after a failure and returning failure if any command fails. It SHALL discover nested workspace boundaries from the root .bazelignore and MODULE.bazel files. Runner infrastructure acceptance probes SHALL NOT be part of normal CI.

  • WHEN a repository CI workflow runs
  • THEN the shared command builds and tests each workspace with the CI profile
  • AND normal Bazel exclusions for manual and incompatible targets apply
  • WHEN one workspace’s build phase fails
  • THEN its test phase and the remaining workspaces are still attempted
  • AND the entry point reports failure

20 - threexui

3x-ui

The architecture source is one diagram with three separate sections: overview, control plane, and data plane. It describes maintained architecture, not live inventory or service health. Bazel renders light and dark SVGs.

The overview shows Master in the Control Plane hierarchy and full node hostnames in provider groups, with Vault, Let’s Encrypt, and Mullvad as external services. Overview links attach to the Control Plane and Data plane boundaries. The control-plane section expands the 3x-ui VM’s Traefik and panel with one simple data-plane node. The data-plane section expands one node’s Traefik, 3x-ui, and Xray with one simple control-plane endpoint; njalla1, work1, and yc1 share this implementation.

3x-ui architecture3x-ui architecture

The data-plane nodes njalla1 and work1 are hosted by njal.la; yc1 is hosted by Yandex Cloud. Terraform configures each through the shared node module; Ansible installs the node services.

work1.nodes.threexui.alwaldend.com is declared at IPv4 45.142.142.192 and IPv6 2a0a:3840:8078:142::2d8e:8ec0:1337 in dnsconfig.json. The node uses the njalla1 Mullvad relay selection with de-fra-wg-001 replacing the retired de-dus-wg-003 relay. Before configuring it, supply its key and address in the existing Vault-backed mullvad_keys map. The node_xui_tokens entry is needed for later master registration, after the node is installed. The node token comes from the xui_tokens field of alwaldend.com/vault1/approles/src_infra_threexui/nodes, and its Mullvad key and address come from the keys field of alwaldend.com/vault1/approles/src_infra_threexui/mullvad_keys (mount secrets).

bazel run //infra/threexui:fix_subs -- --hosts njalla1.nodes.threexui.alwaldend.com,yc1.nodes.threexui.alwaldend.com --sub_id subid
bazel run //infra/threexui:fix_subs -- --sub_file path_to_file

20.1 - Ansible

Ansible setup

20.2 - OpenSpec

20.2.1 - Specifications

20.2.1.1 - Infra threexui

20.2.1.1.1 - Infrastructure 3x-ui

Describe 3x-ui provisioning, host deployment, node routing configuration, and the subscription URL transformer. This baseline describes checked-in source, not verified live node access. The advertised host-only subscription command has a source limitation: sub/main.go also attempts to read sub_file when it is empty, so successful host-only operation is not a baseline guarantee.

Baseline source revision: 550d7e79b1f5fdbc2b6017b75178471d6914082f. Observation date: 2026-09-08. Sources are linked in full; no excerpts are used.

Sources: component documentation, root wrappers, provisioning package, configuration package, host playbook, Traefik routes, node definitions, node inbounds, node routing, and subscription transformer.

The package SHALL keep infrastructure creation in tf_setup, host deployment in Ansible, and 3x-ui API resources in tf. The deployment playbook SHALL apply host, Traefik, and 3x-ui roles to the control-plane and data-plane inventory groups.

  • WHEN an authorized operator selects the setup or main Terraform wrapper
  • THEN its AL plugin selects the corresponding Terraform stage
  • AND the Ansible playbook remains the owner of host and service installation

The Traefik route template SHALL forward the panel to loopback port 2053 and subscription requests to loopback port 2096. It SHALL define HTTP inbound routes for ports 40000 through 40149 and TCP inbound routes for ports 40150 through 40299, forwarding to corresponding loopback ports.

  • WHEN the checked-in dynamic Traefik template is rendered
  • THEN requests matching the configured subscription path prefix route to http://127.0.0.1:2096
  • AND the router selects the configured TLS certificate resolver

Terraform SHALL configure the njalla1 and yc1 nodes through the shared node module. The node routing declaration SHALL block the advertising category and private IP destinations before routing the freedom inbound directly and each mullvad_min or http_proxy inbound through its named WireGuard outbound. The enabled mullvad_min_lb inbound has no active dedicated balancing rule; commented balancing rules SHALL NOT be treated as enabled routing behavior.

  • WHEN a node’s selected relay creates a Mullvad inbound
  • THEN the inbound listens on loopback
  • AND its routing rule selects the matching out-mullvad-min- outbound tag
  • AND the node declaration supplies its own relay selection to the module

For readable local subscription files, the transformer SHALL ignore blank and comment lines, parse each remaining URL, set security=tls and sni to the URL hostname, sort URLs by fragment, and write one URL per output line. It SHALL return a nonzero exit status when input reading or URL parsing fails.

  • WHEN fix_subs receives a readable --sub_file containing subscription URLs
  • THEN output URL queries contain security=tls and the matching hostname SNI
  • AND output is ordered by URL fragment with blank and comment lines omitted

20.2.1.2 - Owned dns

20.2.1.2.1 - owned-dns Specification

Define retained 3x-ui DNS ownership through the owner’s tf_setup root, including canonical declarations, scoped execution, and offline source checks.

The tf_setup root SHALL consume this owner’s canonical dnsconfig.json through the shared DNS Terraform module, preserving declared record identities and views. DNS resources SHALL default to disabled before the adoption revision and SHALL retain enabled ownership after authorized adoption into the owner’s state. Reconciliation against adopted state and unchanged declarations SHALL propose no record additions, changes, replacements, or deletions.

  • WHEN the preparatory root is evaluated with default inputs before its adoption revision
  • THEN it reads this owner’s declaration and disables managed DNS records
  • AND the package contains the module and declaration inputs
  • WHEN the adopted root is evaluated with its checked-in default inputs
  • THEN DNS ownership is enabled and the root reads the canonical declaration
  • AND the package contains the module and declaration inputs
  • WHEN authorized adoption coordinates the previous writers and enables this owner’s record ownership
  • THEN the owner imports existing provider identities into its own state through the owning scoped DNS wrappers and reviewed saved-plan import blocks
  • AND existing record values and declared DNS views are preserved
  • WHEN the owner plans against its adopted state and unchanged declarations
  • THEN it proposes no record additions, changes, replacements, or deletions
  • AND unrelated records remain unchanged

The DNS wrappers SHALL select src_infra_threexui through the repository AL flow and keep secret values in injected variables. dns.plan, dns.show, and dns.apply SHALL retain the owning root and backend, select dns=1, and scope planning to module.dns. Apply SHALL require the reviewed saved plan. This workflow SHALL preserve ordinary service authentication and SHALL NOT establish overall service health. Required DNS credentials and Vault grants SHALL remain operational prerequisites, and RouterOS DNS credentials SHALL remain isolated from unrelated RouterOS resources. The package SHALL expose a format test without Vault authentication or DNS provider access.

  • WHEN the package format test executes
  • THEN it checks the packaged Terraform configuration without live credentials
  • AND existing non-DNS authentication and backend paths remain unchanged
  • WHEN the operator selects the owning DNS wrappers
  • THEN they select DNS credential injection through the owner AppRole with dns=1
  • AND their scoped plan validates DNS and its dependencies
  • AND disabled DNS resources do not imply offline provider configuration

20.3 - sub

Fetch subscriptions and fix them

20.4 - Tf

Terraform config

20.5 - Tf setup

Terraform setup

Use this package’s dns.plan, dns.show, and dns.apply targets for the scoped DNS adoption workflow.

DNS ownership is enabled by default after the verified 2026-09-13 adoption. The adoption evidence records 12 imports, a no-change follow-up plan, and preserved provider inventories. The DNS wrappers select dns=1 and module.dns within this setup root and backend; their validation covers DNS and its dependencies. Apply requires a reviewed saved plan, and ordinary service authentication remains separate.

21 - Vault

Setup for vault.dc1.alwaldend.com

This diagram describes the maintained architecture source, not live inventory or service health. The Mermaid source is authoritative; Bazel renders its light and dark SVGs.

Vault architectureVault architecture

bazel run //infra/vault/tf_setup:tf.apply # Create VMs (requires an active Vault host)
bazel run //infra/vault/ansible # Set up hosts (BM and VMs)
bazel run //infra/vault/tf:tf.apply # Configure vault
bazel run //infra/vault:backup

With a working Vault:

bazel run //infra/vault:unseal

Without a working Vault:

bazel run //infra/vault:unseal_standalone
bazel run //infra/vault/ansible:fix_quorum
bazel run //infra/vault/ansible:ansible.vm # Set up only VMs
bazel run //infra/vault/ansible:ansible.bm # Set up only bare metal

Plan:

bazel run //infra/vault/tf:tf.plan

Apply:

bazel run //infra/vault/tf:tf.apply

Run terraform directly:

bazel run //infra/vault/tf:tf.direct -- -chdir="${PWD}" plan
bazel run //infra/vault/tf:tf.apply -- -replace 'module.vm_ha["host2"].proxmox_vm_qemu.vm' -replace 'module.vm_ha["host3"].proxmox_vm_qemu.vm
username="username"
bazel run //infra/vault:gen_client_cert -- --user "${username}" --output_dir "${PWD}"
bazel run //tools/ykman -- piv certificates import 9A "${PWD}/${username}.pfx"
bazel run //tools/ykman -- piv keys import 9A "${PWD}/${username}.pfx"
bazel run //infra/vault:gen_client_cert -- --host some-host --output_dir "${HOME}/.al/client_cert"
bazel run //infra/vault:gen_client_cert -- --host some-host --user username --output_dir "${HOME}/.al/client_cert"
  • Prepare encrypted unseal token
  • Run and input the encrypted token:
    bazel run //infra/vault:unseal
    
  • Prepare encrypted unseal token
  • Run and input the encrypted token:
    bazel run //infra/vault:gen_root_token -- --pgp_key path_to_public_gpg_key_in_base64
    
bazel run //infra/vault -- write -f pki/ica_servers/roles/ica_servers_dc1_pve1/acme/new-eab
bazel run //:vault -- write ssh/clients/sign/admins ttl=30000000  public_key=@"${HOME}/.ssh/key"
bazel run //infra/vault -- token revoke -mode=path auth

Renew the Vault server certificate through its Ansible role. The vault_tls tag issues a certificate, installs its chain and private key, and reloads Vault with SIGHUP. It stops on certificate errors and does not run the host setup, package installation, or service restart tasks.

Run from the repository root. Keep SSH host verification enabled by deriving a temporary known_hosts entry from the repository-owned public SSH CA:

vault_recovery_dir="${PWD}/out/vault-certificate-recovery"
mkdir -p "${vault_recovery_dir}"
printf '@cert-authority host1.vault.alwaldend.com,host1.vault.dc1.alwaldend.com %s\n' \
  "$(cat infra/vault/tf/output/ssh_ca_servers.crt)" \
  >"${vault_recovery_dir}/ssh_known_hosts"

For an expired server certificate, temporarily disable Vault certificate verification for this recovery command only. The SSH options trust that CA only for this invocation and retain the existing SSH certificate’s host alias:

env VAULT_SKIP_VERIFY=true \
  ANSIBLE_SSH_COMMON_ARGS="-o StrictHostKeyChecking=yes \
-o HostKeyAlias=host1.vault.alwaldend.com \
-o UserKnownHostsFile=\"${vault_recovery_dir}/ssh_known_hosts\"" \
  bazel_agent bazel run //infra/vault/ansible:ansible -- \
  --limit host1.vault.alwaldend.com --tags vault_tls

Vault must be reachable and unsealed, and the host token must allow the component’s AppRole login. If that token has expired, connect the login YubiKey and refresh it first:

bazel_agent bazel run //tools/vault/login -- simeonwarren --insecure

This login replaces the host token. Stop if it fails; a missing YubiKey cannot be fixed by disabling TLS verification. Neither command changes the persistent TLS verification configuration. Do not add the bypass to shell profiles, al.lua, or Ansible defaults, and do not use verbose or diff output for the certificate tasks. The temporary public known_hosts projection does not modify the host’s SSH configuration; do not replace it with an unverified ssh-keyscan result or disable SSH host verification.

After renewal, verify Vault with certificate validation enabled:

env -u VAULT_SKIP_VERIFY bazel_agent bazel run //:vault.status

For renewal before expiry, omit VAULT_SKIP_VERIFY=true from the Ansible command. The current inventory serves Vault from host1; review the inventory before changing the host limit. Its inventory name remains host1.vault.alwaldend.com to preserve the Vault node identity; ansible_host connects through the DNS-owned name host1.vault.dc1.alwaldend.com. Certificate alternate names include this connection hostname for the existing unseal endpoint, as well as the shared Vault service names.

bazel run //infra/vault -- read identity/oidc/client/src_infra_dc1_forgejo1_provider
 bazel run //infra/vault -- list -format json identity/entity-alias/id | jq ".[]" | xargs "-I{}" bazel run //infra/vault -- read "identity/entity-alias/id/{}"

21.1 - Ansible

Ansible setup

21.2 - OpenSpec

21.2.1 - Specifications

21.2.1.1 - Download identity

21.2.1.1.1 - download-identity Specification

Provide a dedicated Vault identity for download infrastructure while keeping state, credential references, and provider permissions scoped to its needs.

Vault configuration SHALL declare src_infra_download using the existing AppRole backend and reusable identity modules. Its state permissions SHALL cover the component’s own environment and DNS state keys, not another component’s state namespace.

  • WHEN the download identity’s effective state policy is evaluated
  • THEN it grants access to download-owned state and locks
  • AND it does not grant access to unrelated component state

The identity SHALL expose the membership and identity outputs needed for Yandex folder provisioning and XCP-ng resource-set assignment. It SHALL receive the required component SSH permissions and DNS provider-secret reads for global and local records without administrative provider credentials.

  • WHEN provider owners consume the declared download identity
  • THEN they can assign its Yandex folder and XCP-ng resource set
  • AND no unrelated component’s identity is reused for deployment

Checked-in declarations SHALL contain secret references only. Download certificate automation SHALL receive role-scoped EAB access for internal Vault HTTP-01 certificates. The role SHALL permit only alwaldend.com, download.alwaldend.com, and www.alwaldend.com, without subdomain or client certificate permissions. Public Let’s Encrypt HTTP-01 SHALL require no DNS challenge credential. Adding this identity SHALL NOT broaden unrelated identities’ policies.

  • WHEN the role and its policy declarations are inspected
  • THEN they name Vault paths and permissions without embedding credential values
  • AND unrelated identity permissions remain unchanged

The download identity SHALL be deployed from its own Terraform root and backend under infra/vault/approles/src_infra_download. Shared identity IDs SHALL be resolved by name through data sources rather than parent-module inputs or another root’s state. Shared group membership lists SHALL retain one owner in the core Vault stage.

  • WHEN shared Vault mounts and the administrative/operator identities already exist
  • THEN the standalone root can create the download identity using administrative authentication
  • AND a subsequent core Vault apply resolves the new identity/group by name and adds their memberships

21.2.1.2 - Infra vault

21.2.1.2.1 - Infrastructure Vault

Describe the Vault host configuration, authentication and certificate services, and packaged recovery entry points. This baseline concerns checked-in desired state and supported wrapper structure; it does not establish live unsealed state, successful backups, quorum, or certificate freshness.

Baseline source revision: 550d7e79b1f5fdbc2b6017b75178471d6914082f. Observation date: 2026-09-08. Sources are linked in full; no excerpts are used.

Sources: operator documentation, operational wrappers, Ansible entry points, Vault host template, authentication backends, storage engines, server PKI, client PKI, and public CA outputs.

The documented workflow SHALL separate tf_setup VM provisioning, Ansible host setup, and the tf Vault configuration stage. Ansible SHALL expose combined, VM-only, and bare-metal-only setup entry points using the shared host and Vault roles.

  • WHEN an operator selects ansible.vm or ansible.bm
  • THEN the wrapper selects the matching checked-in playbook and inventory
  • AND Vault’s API configuration remains owned by the separate Terraform stage

The host template SHALL configure TLS API and cluster endpoints on ports 8200 and 8201 and Raft storage under /opt/vault/raft. It SHALL derive each Raft node identifier from the inventory hostname and emit retry-join addresses for hosts in the Vault inventory group.

  • WHEN Ansible renders vault.hcl for a member of the Vault group
  • THEN its API and cluster URLs use its inventory hostname
  • AND Raft receives that hostname as node ID and the group’s HTTPS join URLs

Terraform SHALL declare userpass, approle, and cert authentication backends, a version-two KV engine at secrets, and a transit engine at transit/default. Client and server certificate authorities SHALL remain distinct, and server PKI ACME SHALL require external account binding with the default directory policy set to forbid.

  • WHEN the main Terraform configuration is evaluated
  • THEN authentication, KV, transit, client PKI, and server PKI are distinct declared resources
  • AND server ACME is enabled with eab_policy = "always-required"
  • AND published CA output files contain certificate or public-key material

The package SHALL expose separate wrappers for backup, ordinary unseal, standalone unseal, client certificate generation, and root-token generation. Standalone unseal SHALL use the default_no_auth environment without selecting the ordinary unseal plugin; these wrappers SHALL NOT imply that recovery has been executed successfully.

  • WHEN an authorized operator invokes //infra/vault:unseal_standalone
  • THEN the wrapper selects the packaged unseal utility and declared Vault endpoint with its no-auth environment
  • AND success remains an operational result to be observed separately

Every owner that manages DNS records through Terraform SHALL use its existing component AppRole or a dedicated owner AppRole when one is missing. A DNS-only identity SHALL retain the shared AppRole module’s own-state and named shared secret access without cloud provisioning, SSH, or PKI permissions. Existing component identities and unrelated authentication flows SHALL remain stable.

  • WHEN a project has DNS records but no existing component AppRole
  • THEN Vault configuration declares an AppRole named for that owner
  • AND the identity can manage its own Terraform state without access to another owner’s state
  • WHEN host_bot uses its dedicated DNS AppRole
  • THEN its existing Ansible authentication remains unchanged

DNS identities SHALL receive read access to the existing Cloudflare credential only when they own global records and to the existing RouterOS DNS credential only when they own dc1 records. DNS access SHALL NOT grant writes to either provider secret or duplicate credential values.

  • WHEN an owner declares only global records
  • THEN its DNS policy grants Cloudflare credential reads
  • AND its DNS policy grants no RouterOS credential access
  • WHEN an owner declares global and dc1 records
  • THEN its DNS policy grants reads for both existing credential references
  • AND neither read policy grants mutation of the provider credential

The packaged Vault Ansible workflow SHALL expose a vault_tls tag that issues and installs the configured server certificate and private key, then reloads Vault. Selecting only this tag SHALL NOT update packages, the service binary, host configuration, or Raft peers. Certificate material SHALL be suppressed from task output. Explicit certificate renewal SHALL stop on issuance or installation failure before reloading the service.

  • WHEN the operator selects vault_tls and limits the play to one Vault host
  • THEN only certificate issuance, certificate and key installation, and service reload are selected, in addition to read-only fact gathering
  • AND a failed certificate task prevents the reload

The recovery procedure SHALL use an invocation-scoped verification override only when an expired server certificate prevents the authorized renewal. It SHALL retain the existing Vault authentication and injection flow, require a valid bootstrap login, and verify the recovered service with certificate validation restored. The procedure SHALL NOT persist insecure TLS defaults.

  • WHEN an authorized operator performs the documented recovery
  • THEN the temporary override applies to credential injection and certificate issuance within that command
  • AND a failed bootstrap login prevents proceeding to renewal
  • AND success is established separately with normal TLS verification

21.2.1.3 - Owned dns

21.2.1.3.1 - owned-dns Specification

Define Vault service DNS management through the owner’s tf_setup root, including canonical declarations, scoped credentials, and offline source checks before adopting live records.

The tf_setup root SHALL consume this owner’s canonical dnsconfig.json through the shared DNS Terraform module, preserving declared record identities and views. DNS resources SHALL default to disabled until the authorized adoption revision and SHALL retain enabled ownership after existing records are imported into the owner’s state. Reconciliation against unchanged declarations and adopted state SHALL propose no record additions, changes, replacements, or deletions.

  • WHEN the checked-in root is evaluated with default inputs before its authorized adoption revision
  • THEN it reads this owner’s declaration and disables managed DNS records
  • AND the package contains the module and declaration inputs
  • WHEN the authorized adoption completes the shared writer audit and applicable recovery prerequisites and enables the owner’s record management
  • THEN the owning root imports existing records by exact provider ID into its state while preserving their declared identities and views
  • WHEN the adopted root uses its checked-in source defaults
  • THEN DNS ownership remains enabled and the package retains the module and canonical declaration inputs
  • WHEN the owning root plans against its adopted state and unchanged declarations
  • THEN it proposes no record additions, changes, replacements, or deletions

The dns.plan, dns.show, and dns.apply entrypoints SHALL select src_infra_dc1_vault through the repository AL flow using dns=1 and operate on module.dns in the existing tf_setup root and Vault HTTP backend. The apply entrypoint SHALL require a saved plan. Ordinary setup and service wrappers SHALL retain their stage labels, authentication, and backend paths. Secret values SHALL remain in injected variables. Real DNS credentials and Vault policy grants SHALL be prerequisites for operational DNS calls. RouterOS DNS credentials SHALL remain isolated from unrelated RouterOS resources. The package SHALL expose a format test that does not authenticate to Vault or contact DNS providers.

  • WHEN the package format test executes
  • THEN it checks the packaged Terraform configuration without live credentials
  • AND existing non-DNS authentication and backend paths remain unchanged
  • WHEN a scoped DNS entrypoint selects dns=1
  • THEN it selects DNS credential injection through the owner AppRole and existing setup backend, with plan and show scoped to module.dns and apply requiring a saved plan
  • AND disabled DNS resources do not imply offline provider configuration
  • AND ordinary setup and service authentication retains its original labels

21.3 - Tf

Terraform config

Missing DNS-owner AppRoles compose the existing reusable AppRole module with an owner-specific DNS policy in approles/<name>/. DNS access grants read-only access to the existing provider credentials for the owner’s views. Existing component identities receive these policies through their existing module declarations.

DNS-only group membership adds no policies. These identities retain their own state and named shared-secret access without joining the general infrastructure AppRole group or receiving cloud provisioning, SSH, or PKI permissions.

The operator bootstrap membership follows the existing component pattern. An authorized Vault apply must create the roles and policies before their DNS stages can authenticate; source validation does not establish deployed access. Follow the DNS migration for the coordinated cutover and the AppRole bootstrap procedure when distinguishing missing deployment from insufficient operator access.

21.3.1 - Output

Output files

21.4 - Tf setup

Terraform setup

This root owns Vault’s canonical DNS declaration and retains its existing src_infra_dc1_vault AppRole and setup state backend. Use //infra/vault/tf_setup:dns.plan, dns.show, and dns.apply for the scoped DNS workflow. They select dns=1 and module.dns; apply requires a reviewed saved plan. Ordinary setup and service wrappers retain their existing authentication flow.

dns_enabled defaults to true after verified adoption. Keep it enabled to retain existing records; disabling it would propose deletion. The adoption record contains the import and verification evidence. Targeted DNS checks do not establish the health of other resources in the root.

Offline formatting is available through //infra/vault/tf_setup:tf_tests.fmt_test.

22 - XCP-ng

XCP-ng infrastructure

Terraform in tf manages a Xen Orchestra resource set for every entity in Vault’s approles group, following infra/pve/tf. Resource sets are the XO grouping and delegation mechanism; XCP-ng host pools represent physical clusters and are not created per AppRole.

resource_set_inventory assigns a pool, template, storage repository and network by name to each AppRole. Native provider lookups resolve their IDs and reject ambiguous matches. Forgejo and OpenHands have assignments by default; other sets remain empty. Subjects are the exact synchronized OIDC users selected by Vault issuer and immutable AppRole entity UUID. Bootstrap has synchronized the 18 AppRole users; new AppRoles need their first OIDC login before Terraform can bind their sets. resource_set_cpu_limit defaults to 32 CPUs per set. Creating a set does not allocate CPUs or storage.

The provider is pinned to vatesfr/xenorchestra 0.41.0. The single infra/xcp_ng/al.lua configuration supplies administrator authentication for XCP-ng infrastructure operations: its named xcp_ng Vault AppRole injects XOA_TOKEN from key xoa_token in secrets/alwaldend.com/vault1/approles/src_infra_xcp_ng/xoa. Forgejo instead uses its own Vault identity through the XO login plugin, which issues an invocation-scoped XO session and revokes it on shutdown. It retains its own Terraform backend and does not inherit the infrastructure administrator token. Token values are not Terraform inputs or outputs.

The default endpoint is wss://xoa.xcp-ng.alwaldend.com. DNS maps the XO appliance to 192.168.1.206 and the XCP-ng host to 192.168.1.213. TLS verification is enabled. xoa_insecure is an explicit temporary bootstrap override for an appliance whose certificate has not been replaced. The HTTP backend stores state under the AppRole’s tf_backend Vault path.

infra/vault/tf owns the src_infra_xcp_ng_provider OIDC client, the src_infra_xcp_ng_users and src_infra_xcp_ng_admins groups, and the src_infra_xcp_ng_pki_server ACME role. Apply those scoped prerequisites before the XO configuration. The callback is https://xoa.xcp-ng.alwaldend.com/signin/oidc/callback.

XO must have the auth-oidc plugin installed and trust Vault’s CA. The Terraform provider has no plugin resource, so terraform_data.oidc invokes the packaged Go JSON-RPC helper directly, without a shell. It configures Vault discovery, the client, and the user groups scopes, enables autoload, and verifies the plugin is loaded. The OIDC client secret remains sensitive and is stored in the Vault-protected Terraform state. The provisioner signs the XCP-ng AppRole in through Vault to create the synchronized OIDC groups. Plugin reconciliation runs when configuration or the helper binary changes, without native drift refresh. Use -replace=terraform_data.oidc to reapply after manual plugin drift.

Native xenorchestra_acl resources manage the administrators group’s pool permissions. An import block adopts matching existing ACLs using their discovered API IDs. The helper only reads these ACLs; it does not change them. The users group grants login eligibility, with membership refreshed at login. Per-AppRole resource-set membership and existing VM ownership use direct user subjects. The Forgejo user receives administration permission on its own VM; no shared pool-wide view grant is configured.

The helper remains for gaps in the pinned provider: OIDC plugin configuration and bootstrap, discovery of external identity subjects and synchronized groups, host registration, prepared VHD template import, and diagnostics. Resources, ACLs and named infrastructure lookups use the native provider.

The resource-set management page is Self Service. In the deployed XO source revision 961b505cfb74cbf24aaeb8d61c5613f97e78de2c, its menu entry requires a global administrator. The page also requires XOA_PLAN > 3, corresponding to Premium or Community. The runtime edition has not been verified. Pool-level administrator ACLs do not satisfy the page’s global-administrator requirement.

The certificate playbook uses Certbot with Vault EAB, installs XO’s Vault CA trust, and configures renewal. It requires appliance SSH access and inspected HTTPS certificate paths. Initial issuance and renewal briefly interrupt XO management access, not running guest VMs.

The XO server must be connected to an XCP-ng host before discovering templates or creating VMs. xo_inspect prints selected operational metadata without credentials or plugin configuration:

XOA_URL=wss://xoa.xcp-ng.alwaldend.com bazel_agent bazel run //infra/xcp_ng:xo_inspect
bazel_agent bazel run //infra/xcp_ng/tf:tf.plan
bazel_agent bazel run //infra/xcp_ng/tf:tf.apply
bazel_agent bazel test //infra/xcp_ng/tf:tf_tests.fmt_test //infra/xcp_ng/cmd/xo_config:xo_config_test

Formatting and helper tests run without live authentication. See Forgejo provisioning for VM prerequisites.

To register host1, store its xcpng_username and xcpng_password fields in secrets/alwaldend.com/vault1/approles/src_infra_xcp_ng/host1, then run:

XOA_URL=wss://xoa.xcp-ng.alwaldend.com XCPNG_HOST=192.168.1.213 bazel_agent bazel run //infra/xcp_ng:xo_register_host

The command uses the XOA token to register the host and supplies its separate host credentials through AL. It waits for a connected pool and leaves an already connected host unchanged. Duplicate registrations are rejected. XCPNG_INSECURE=true explicitly allows the host’s bootstrap certificate; XOA_INSECURE=true separately controls verification of XO’s certificate.

//infra/xcp_ng:xo_import_template imports the repository-pinned image from //third_party/org_fedora_cloud through XO’s disk-upload API. Supply XO_TEMPLATE_SHA256 from the image’s pinned checksum, XO_TEMPLATE_SR, XO_BASE_TEMPLATE_ID (a diskless Generic Linux BIOS template), and XO_TEMPLATE_NAME, together with XOA_URL. The host must support QCOW2 imports. The command verifies the image before creating anything, creates an unbooted template with only a boot disk at device 0, and verifies the final disk layout. It rejects incomplete or conflicting prior imports for inspection. This keeps cloud-init fresh for cloned VMs and prevents empty CD drives from shifting the Forgejo data disks to unexpected device names.

Older XO releases, including 5.192.1, require VHD rather than QCOW2 uploads. For those releases, convert the pinned image to dynamic VHD in task-private scratch, verify the decoded contents against the original with qemu-img compare, and record the converter version and converted file’s SHA256. Use //infra/xcp_ng:xo_import_prepared_template with XO_TEMPLATE_IMAGE set to the absolute converted path, XO_TEMPLATE_FORMAT=vhd, and XO_TEMPLATE_SHA256 set to the converted checksum. The other inputs are the same as for the pinned-image importer.

An interrupted import can resume on an inspected empty, halted task VM by setting XO_TEMPLATE_RESUME_ID, XO_TEMPLATE_PREVIOUS_FORMAT, and XO_TEMPLATE_PREVIOUS_SHA256 to its exact prior identity and image marker. Resume preserves that VM and refuses any attached disk or network interface.

22.1 - OpenSpec

22.1.1 - Specifications

22.1.1.1 - Infra xcp ng

22.1.1.1.1 - XCP-ng infrastructure Specification

Specify Xen Orchestra resource delegation, scoped authentication, and certificate deployment owned by infra/xcp_ng. The baseline is checked-in source at revision 550d7e79b1f5fdbc2b6017b75178471d6914082f, observed on 2026-09-08. Existing operational observations in project documentation are historical context; this baseline does not verify current deployment, edition, identity synchronization, or service health.

Terraform SHALL declare one Xen Orchestra resource set per entity in Vault’s approles group. Each set SHALL contain its explicitly assigned template, storage repository, and network resolved by names within the selected pool; unassigned sets SHALL contain no inventory objects. Each set SHALL have a positive integer CPU quota, defaulting to 32. These resource sets SHALL represent delegation groups rather than new physical host pools.

Sources: resource sets, named inventory, and project contract.

  • WHEN an AppRole exists in Vault’s group but has no resource_set_inventory entry
  • THEN its resource set contains no assigned inventory objects and retains the configured CPU quota.

Resource-set membership SHALL select the exact synchronized OIDC user by Vault issuer and immutable AppRole entity UUID. AppRoles without synchronized users SHALL be reported in approles_pending_oidc_login and have no resource-set subjects. Existing-VM owner ACLs SHALL require exactly one VM matching the hostname and owning AppRole tag within its named pool, and SHALL grant admin to the owner’s synchronized user. Unknown owners, ambiguous VMs, or missing owner synchronization SHALL fail planning. Native Terraform ACL resources SHALL separately manage the synchronized administrators group’s pool grants.

Sources: identity contract, owner ACLs and preconditions, resource-set subjects, and administrator ACLs.

  • WHEN identity discovery cannot find its synchronized OIDC subject
  • THEN its resource set has no subjects and its name appears in the pending login output; an existing-VM ownership grant for it fails its precondition.
  • WHEN hostname and AppRole tag matching in the named pool returns zero or multiple VMs
  • THEN Terraform rejects the existing-VM ownership configuration.

The XO login plugin SHALL authenticate using the calling component’s Vault identity and export XOA_TOKEN, XOA_URL, and XOA_INSECURE=false to the invoked process. XO HTTPS verification SHALL be mandatory for this plugin. Session cookies and tokens SHALL remain in process memory, and normal plugin shutdown SHALL revoke only the XO token issued by that invocation. Tenant authentication SHALL remain separate from the infrastructure administrator token. Forced termination or a lost callback can leave a session until XO’s configured expiry and SHALL not be described as guaranteed revocation.

Sources: XO login contract and authentication ownership.

  • WHEN normal plugin shutdown follows a successful XO login
  • THEN the plugin revokes its issued session using token.deleteOwn without revoking unrelated sessions.

The XO appliance playbook SHALL use Certbot with Vault external account binding, retain the inspected listener certificate paths, install service-specific Vault CA trust, and configure twice-daily renewal checks. The host playbook SHALL use its packaged compatibility helper and the checksum-verified Lego release to install certificates through XAPI’s supported certificate-install command. Temporary EAB registration material SHALL be removed after use. Appliance renewal may interrupt XO management access; host renewal SHALL not stop XAPI or running VMs.

Sources: certificate deployment contract and packaged playbooks and runtime inputs.

  • WHEN the renewal helper obtains a certificate different from the installed certificate
  • THEN it uses xe host-server-certificate-install, with installation retry tracked independently of issuance, while preserving running VMs.

22.1.1.2 - Owned dns

22.1.1.2.1 - owned-dns Specification

Define XCP-ng infrastructure DNS management through the owner’s tf root, including canonical declarations, scoped execution, and offline source checks through preparation, adoption, and ongoing reconciliation.

The tf root SHALL consume this owner’s canonical dnsconfig.json through the shared DNS Terraform module, preserving declared record identities and views. DNS resources SHALL default to disabled until the authorized adoption revision and SHALL retain enabled ownership after existing records are imported into the owner’s state. Reconciliation against unchanged declarations and adopted state SHALL propose no record additions, changes, replacements, or deletions.

  • WHEN the checked-in root is evaluated with default inputs before its authorized adoption revision
  • THEN it reads this owner’s declaration and disables managed DNS records
  • AND the package contains the module and declaration inputs
  • WHEN the authorized adoption audits competing writers, controls those identified within its observed scope, and enables the owner’s record management
  • THEN the owning root imports existing records by exact provider ID into its state while preserving their declared identities and views
  • WHEN the adopted root uses its checked-in source defaults
  • THEN DNS ownership remains enabled and the package retains the module and canonical declaration inputs
  • WHEN the owning root plans against its adopted state and unchanged declarations
  • THEN it proposes no record additions, changes, replacements, or deletions

The DNS-only plan, show, and saved-plan apply commands SHALL select dns=1 through the repository AL flow, using src_infra_xcp_ng through its named xcp_ng Vault authentication. They SHALL target module.dns in the existing tf root and backend and keep secret values in injected variables. The ordinary service wrappers SHALL retain their existing xcp_ng_tf, xoa, and Vault environment labels and authentication behavior. Real DNS credentials and Vault policy grants SHALL be prerequisites for operational Terraform calls. RouterOS DNS credentials SHALL be isolated from unrelated RouterOS resources. The package SHALL expose a format test that does not authenticate to Vault or contact DNS providers. Successful DNS-only execution SHALL establish only the scoped DNS result, not full service health.

  • WHEN the package format test executes
  • THEN it checks the packaged Terraform configuration without live credentials
  • AND existing non-DNS authentication and backend paths remain unchanged
  • WHEN the DNS wrapper selects dns=1
  • THEN it selects DNS credential injection through the owner AppRole while preserving the existing root and backend without starting XO authentication
  • AND disabled DNS resources do not imply offline provider configuration
  • WHEN the operator reviews and applies a saved plan through the DNS-only plan, show, and apply commands
  • THEN the operation is limited to module.dns and its dependencies
  • AND its success does not establish the health of the other services in the root

22.2 - Terraform

AppRole resource sets and Vault-backed Xen Orchestra authentication

This configuration manages one Xen Orchestra resource set per Vault AppRole and configures the installed XO OIDC plugin through a packaged JSON-RPC helper. See the project documentation for requirements and checks.

Resource-set subjects reference exact synchronized OIDC users whose OIDC subject identifiers match Vault AppRole entity UUIDs. AppRole groups can include other service entities, so using those groups would grant access across deployments. Bootstrap XO’s OIDC configuration first, then log in through OIDC as each AppRole and run the full Terraform apply. Identity discovery runs at plan time and never creates users. AppRoles that have not logged in are listed in approles_pending_oidc_login; their sets remain without subjects until the next apply after login. Groups remain responsible for login entitlement and administrative access, separately from these per-AppRole grants.

resource_set_inventory supplies pool, template, storage and network names per AppRole. Native data sources resolve a unique pool and unique objects within that pool. Unassigned resource sets remain empty. These inputs have no UUID defaults; resolved IDs remain necessary for XO API operations.

existing_vms maps hostnames to an owning AppRole and pool name. Discovery requires exactly one VM with both the hostname and AppRole tag in that pool. The owner receives an explicit admin ACL. Resource-set membership permits provisioning and does not transfer existing VM ownership. Missing or ambiguous VMs, unknown AppRoles and unsynchronized OIDC users fail planning. ACL resource keys retain the AppRole and resolved VM ID, preserving their addresses when the same existing VMs are found through names.

Native xenorchestra_acl.pool_administrator resources manage the synchronized administrators group’s permissions on connected pools. The read-only helper identifies existing matching ACLs, and an import block adopts them without recreating access. Fresh installations must bootstrap terraform_data.oidc before the full plan so the synchronized groups exist. There is no shared pool-view grant and the helper performs no ACL writes.

The pinned hashicorp/external provider calls the packaged helper’s read-only oidc-identities command. XO exposes login names as email and authentication providers in authProviders; the helper accepts only OIDC-authenticated identities from the configured Vault issuer and indexes them by authProviders["oidc:" + issuer].id, the Vault entity UUID. Duplicate OIDC subjects are an error. Mutable login names and inherited group membership do not select the ACL subject. Group discovery selects only synchronized OIDC groups and rejects duplicate names. The provider has no group datasource and its user datasource does not expose immutable external subjects. API credentials remain in the injected environment.

Use this package’s dns.plan, dns.show, and dns.apply targets for the scoped DNS adoption workflow. These commands select dns=1 and target module.dns in this same root and backend, using the existing src_infra_xcp_ng AppRole through named xcp_ng Vault authentication. The ordinary service wrappers retain their XO authentication and labels. A successful DNS-only operation verifies its scoped DNS result; full service health requires the service’s own checks.

DNS ownership defaults to enabled after the completed record adoption. Keep that default enabled to retain the imported records.

22.3 - Xen Orchestra certificates

Vault ACME certificate issuance and renewal for the XOA appliance

This playbook installs Debian’s Certbot package and obtains a certificate for xoa.xcp-ng.alwaldend.com and host1.xoa.xcp-ng.alwaldend.com from the Vault src_infra_xcp_ng_pki_server ACME role. The XCP-ng AppRole creates the external account binding; its registration file is removed after issuance.

The target injects xoa_ssh_password from secrets/alwaldend.com/vault1/approles/src_infra_xcp_ng/xoa for the appliance’s xoa SSH account and sudo. Ansible uses its sshpass password mechanism; the controller must provide sshpass and a verified SSH host key. Passwords are runtime environment inputs and are not stored in inventory.

Run the read-only inspection target before deploying a different appliance:

bazel_agent bazel run //infra/xcp_ng/ansible:ansible.inspect
bazel_agent bazel run //infra/xcp_ng/ansible:ansible

The checked-in settings match the inspected appliance: xo-server runs as root and listens on HTTPS with /etc/ssl/cert.pem and /etc/ssl/key.pem. Native autoCert is disabled. The deployment preserves these listener paths. Apply the Vault PKI role before certificate issuance.

Certbot uses standalone HTTP-01 validation. Vault must resolve both names to the appliance and reach TCP port 80. Issuance and renewal briefly stop xo-server to free that port, interrupting management access but not guest VMs. The existing TLS files are preserved with .before-vault-acme suffixes before their paths become symlinks to Certbot’s renewable files. The dedicated xo-acme-renew.timer checks twice daily. Certbot is configured to renew the seven-day Vault certificates with two days remaining; stop/start hooks run when renewal is needed. Certificate keys and ACME account files remain root-only.

The playbook adds a service-specific Vault CA trust override using NODE_EXTRA_CA_CERTS so XO can also validate Vault’s OIDC endpoint. It verifies the appliance HTTPS endpoint against that CA after deployment.

The deployment target is //infra/xcp_ng/ansible:ansible. Building //infra/xcp_ng/ansible:ansible_bin packages the playbook without deploying it.

XO’s native ACME implementation does not pass EAB credentials to its ACME client. Certbot supports external account binding without changing Vault’s mandatory-EAB policy.

//infra/xcp_ng/ansible:ansible.host provisions a Vault ACME certificate for host1.xcp-ng.alwaldend.com and xcp-ng.alwaldend.com. Inspect a different host with //infra/xcp_ng/ansible:ansible.inspect_host before deployment. The host must serve /opt/xensource/www/.well-known/acme-challenge through HTTP or an HTTP-to-HTTPS redirect; the inspection verifies and cleans up a public probe file. This host supports that path without changing XAPI settings.

XCP-ng’s Python 3.6 cannot run the current Ansible modules. The playbook uses Ansible’s script transport for a Python 3.6-compatible helper, without adding packages to dom0. The helper downloads the immutable Lego release declared in third_party/com_github_go_acme_lego_bin/binary_toolchain.json, verifies its checksum, and installs it under root-only /etc/xcp-ng-acme.

The initial EAB is temporary. Only the ACME account remains for unattended renewal; no Vault token is retained on the host. The client requests RSA4096 keys to match the Vault role. xcp-ng-acme-renew.timer runs twice daily and uses XAPI’s supported xe host-server-certificate-install command when the issued certificate differs from the installed certificate. Installation is retried independently of issuance. Renewal does not stop XAPI or running VMs. The final deployment verifies HTTPS and enables the timer.

Set an absolute task-private TMPDIR for controller registration scratch. Ansible removes the temporary registration script after execution. The bootstrap tag updates the helper without ordering a certificate.

The XO playbook installs the official xo-server-auth-oidc 0.4.0 plugin with pinned, checksum-verified upstream files and isolated dependencies. This version supports Vault group synchronization while remaining compatible with the installed XO core. Ansible verifies the plugin before restarting XO; Terraform configures it and reconciles group ACLs through the API.

SSH connection reuse keeps deployment within the appliance’s existing UFW limit of six new SSH connections per 30 seconds. No firewall exception is required.

23 - Yandex Cloud

Yandex Cloud (yandex.cloud)

23.1 - OpenSpec

23.1.1 - Infrastructure Yandex Cloud

Describe organization-level Yandex Cloud folder provisioning derived from Vault AppRole identities. The direct infra/yandex_cloud owner currently contains one concrete organization package, org1, covered here. This baseline does not assert that the cloud organization or its folders were inspected live.

Baseline source revision: 550d7e79b1f5fdbc2b6017b75178471d6914082f. Observation date: 2026-09-08. Sources are linked in full; no excerpts are used.

Sources: top-level documentation, organization package, organization targets, Terraform packaging, folder derivation, provider configuration, and credential injection.

The Yandex Cloud tree SHALL expose its documentation through the infrastructure documentation boundary and keep org1’s AL configuration and Terraform package under infra/yandex_cloud/org1.

  • WHEN a contributor follows the Yandex Cloud package’s child documentation
  • THEN org1 identifies the current organization configuration
  • AND its Terraform wrapper uses //infra/yandex_cloud/org1:al

Terraform SHALL read the Vault identity group named approles, resolve its member entities, and instantiate the shared yc_folder module once per entity name. Folder names SHALL replace underscores with hyphens, and secret references SHALL use the corresponding yandex.cloud/org1/folders/ path.

  • WHEN the approles group contains an entity named example_service
  • THEN the folder module receives the name example-service
  • AND its secret reference is yandex.cloud/org1/folders/example-service
  • AND its cloud identifier comes from the configured cloud_id variable

The Terraform Bazel package SHALL include its provider lock, shared folder and backend modules, and Vault backend and injector tools. Its operational wrapper SHALL select the Terraform plugin and the default_default Vault environment; the package SHALL also expose the configured Terraform test map.

Scenario: Resolve the organization Terraform entry point

  • WHEN the organization’s Terraform wrapper is assembled
  • THEN its runfiles contain the declared local modules and provider lock
  • AND runtime authentication is supplied by the owning AL/Vault flow

23.2 - Org1

Organization 1

23.2.1 - Tf

Terraform config