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.

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 - Architecture

Rendered infrastructure architecture diagrams

These diagrams render every page of the checked-in Drawio source. They describe the source document, not a live inventory or health check. Pages marked Archive retain the source’s historical classification. Open an image for its full-size SVG.

DC1 infrastructure diagram

Vault infrastructure diagram

Flux infrastructure diagram

Forgejo infrastructure diagram

www infrastructure diagram

Threexui infrastructure diagram

DNS infrastructure diagram

Ingress infrastructure diagram

T3code infrastructure diagram

Truenas infrastructure diagram

Archive/Harvester infrastructure diagram

Archive/Opencode infrastructure diagram

Archive/Hermes infrastructure diagram

Archive/Proxmox infrastructure diagram

Archive/Harbor infrastructure diagram

Open the canonical source with the pinned Drawio desktop tool:

bazel_agent bazel run //infra/arch

Regenerate the maintained SVGs after editing arch.drawio:

bazel_agent bazel run //infra/arch:update
bazel_agent bazel test //infra/arch:update_tests

The update target renders all 15 named pages in Bazel sandboxes with Drawio 30.2.6 web assets from the pinned desktop archive and the repository’s pinned headless Chrome. A pinned Liberation font set and isolated Fontconfig configuration keep text measurement independent of host fonts. The renderer loads only local assets, fails on missing pages and export errors, and records the source SHA-256 in each SVG. No display server, live infrastructure access, or runtime download is required. The freshness test compares rendered output with the maintained SVGs; documentation consumes those checked-in images.

1.1 -

Infrastructure architecture Specification

Describe the maintained infrastructure diagrams owned by infra/arch. The baseline is checked-in source at revision 550d7e79b1f5fdbc2b6017b75178471d6914082f, observed on 2026-09-08. These diagrams describe their source document; they do not establish live inventory, deployment, or health.

The project SHALL use arch.drawio as the canonical source for its maintained SVG diagrams and SHALL expose the source through the //infra/arch editor target. The named render mapping SHALL preserve all 15 source pages, including the five pages classified as archives.

Sources: project documentation, target and page definitions, and Drawio source.

  • WHEN the //infra/arch:rendered target processes the canonical document
  • THEN it produces the SVG output mapped to each of the 15 named pages, including separate outputs for archived pages.

The project SHALL render diagrams through the repository’s pinned Drawio, headless Chrome, and font inputs. The //infra/arch:update target SHALL update the maintained SVGs, and //infra/arch:update_tests SHALL compare them with freshly rendered output. Rendering SHALL use local assets and report missing pages or export failures.

Sources: rendering guarantees and render and update targets.

  • WHEN a source edit changes a rendered SVG without updating its maintained copy
  • THEN the freshness test reports a mismatch, and the update target provides the regenerated source file.

Documentation SHALL display the maintained SVGs with links to their full-size images and SHALL retain the source’s Archive classification. The diagrams SHALL remain documentation artifacts rather than evidence of live service availability.

Source: diagram documentation.

  • WHEN a reader opens the Proxmox architecture section
  • THEN it is labeled Archive/Proxmox and links to the maintained SVG.

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 -

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.

Cloudflare DNS and Mikrotik DNS list the records this repository declares for each destination view, with the owning declaration for every record. They are generated from dnsconfig.json:

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

//infra/dns:config_test fails when a checked-in page is out of date, so the pages cannot drift from the declarations they project.

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 generated declaration pages above project declared state, and the declarations themselves remain the only inventory.

4.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, and SHALL fail its offline check when a checked-in page differs from the declarations it projects.

  • WHEN an owner changes a declaration and the generation command runs
  • THEN the affected view’s page lists the declared records with their owner
  • AND the check passes against the regenerated page
  • WHEN a checked-in page no longer matches the declarations
  • THEN the offline check fails rather than serving a stale inventory

4.2 - 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.3 - 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.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 - flux

Fluxcd deployment
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
    

5.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

5.2 -

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

5.3 - Ansible

Ansible setup

5.4 - cl

Cluster manifests

5.5 - 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.

6 - forgejo

git.alwaldend.com

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

6.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

6.2 -

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

6.3 - Ansible

Ansible setup

6.4 - 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

6.5 - 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.

7 - Forgejo runner

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

7.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

7.2 -

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

7.3 - Ansible

Forgejo runner configuration

7.4 - 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.

8 - 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.

8.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

8.2 - 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 - 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.

9.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.

10 - harbor

Harbor deployment
  • 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
    

10.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

10.2 -

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

10.3 - Ansible

Ansible setup

10.4 - cl

Cluster manifests

10.5 - Tf

Terraform config

10.6 - 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.

11 - ingress

ingress
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
    

11.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

11.2 -

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

11.3 - Ansible

Ansible setup

11.4 - 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.

12 - 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.

  • 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

12.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

12.2 -

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

12.3 -

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 - Nas

TrueNAS deploy

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

13.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.

13.2 -

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

13.3 -

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 - 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.

14.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

14.2 - 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.

14.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.

15 - Pve

Proxmox cluster pve.alwaldend.com
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"
    

15.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.

15.2 -

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

15.3 - Ansible

Ansible setup

15.4 - 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.

16 - 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.

16.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

16.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

17 - 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

17.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=projects/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 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.

17.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.

17.1.2 -

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

17.1.3 -

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. 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

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

18 - threexui

3x-ui
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

18.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

18.2 -

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

18.3 - Ansible

Ansible setup

18.4 - sub

Fetch subscriptions and fix them

18.5 - Tf

Terraform config

18.6 - 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.

19 - Vault

Setup for vault.dc1.alwaldend.com
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

Vault certificates (tls_cert_file, tls_key_file) should be updated manually

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/{}"

19.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

19.2 -

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

19.3 - Ansible

Ansible setup

19.4 - 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.

19.4.1 - Output

Output files

19.5 - 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.

20 - 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.

20.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.

20.2 -

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

20.3 - 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.

20.4 - 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.

21 - Yandex Cloud

Yandex Cloud (yandex.cloud)

21.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

21.2 - Org1

Organization 1

21.2.1 - Tf

Terraform config