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.
Requirements
- 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.
New project
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

Vault

Flux

Forgejo

www

Threexui

DNS

Ingress

T3code

Truenas

Archive/Harvester

Archive/Opencode

Archive/Hermes

Archive/Proxmox

Archive/Harbor

Edit and regenerate
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
Purpose
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.
Requirements
Requirement: Canonical multi-page architecture source
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.
Scenario: Render the maintained page set
- 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.
Requirement: Reproducible diagram refresh
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.
Scenario: A maintained diagram becomes stale
- 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.
Requirement: Documentation preserves historical classification
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.
Scenario: Consult a historical architecture page
- WHEN a reader opens the Proxmox architecture section
- THEN it is labeled
Archive/Proxmox and links to the maintained SVG.
2 - Ceph
Ceph
Links
See current crush map
Go to Nodes -> Host -> Ceph -> Configuration
Crush map update
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
Purpose
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.
Requirements
Requirement: Single-host CRUSH topology
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.
Scenario: Inspect the recorded storage topology
- 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
Requirement: OSD-level replicated and erasure placement
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.
Scenario: Inspect the placement failure domain
- 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
Requirement: Operator workflow documentation
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.
Scenario: Use the documented map update procedure
- 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.
Declaration pages
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.
Inspect and lint declarations
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.
Migration and recovery
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
Purpose
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.
Requirements
Requirement: Aggregate records from their owning components
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.
Scenario: A new project adds a canonical declaration
- WHEN the runtime linter scans a workspace containing the new file
- THEN it includes the file without requiring a registry update
Requirement: Preserve distinct global and site-local views
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
Requirement: Validate configuration without provider access
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.
Scenario: Validate a changed declaration
- WHEN offline checks run
- THEN they validate the declaration and fixtures without deploying records
Requirement: Inject credentials into operational wrappers
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.
Scenario: A global-only owner operates its root
- 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.
Scenario: An owner has both stages
- WHEN the integration is prepared
- THEN only its setup root declares the DNS module
Requirement: One declaration file owns each DNS name
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.
Scenario: Different files split a name across types or views
- 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
Scenario: One file manages multiple records for a name
- WHEN one file declares several values, types or views for a name
- THEN ownership lint accepts the sole source
Requirement: Preserve live records during transfer
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.
Scenario: An owner is adopted
- 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
Scenario: Deploy verified missing declarations
- 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
Scenario: Inspect the prepared shared-record defaults
- 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
Scenario: Inspect adopted shared-record defaults
- 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
Scenario: Reconcile existing shared records
- 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
Requirement: Avoid a separate DNS exporter
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.
Scenario: Inspect declared ownership
- WHEN the runtime linter succeeds
- THEN its table reports the discovered declarations without an export step
Requirement: Publish declaration pages per destination view
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.
Scenario: Regenerate a declaration page
- 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
Scenario: A declaration page is stale
- 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
Links
Set up port forwarding
ssh -L 6443:127.0.0.1:6443 -N flux.alwaldend.com
Run flux CLI
bazel run //infra/flux/cl:flux
Show flux status
bazel run infra/flux/cl:flux -- get all -A
Run bootstrap
bazel run //infra/flux/cl:flux.bootstrap
Run flux operator
bazel run //infra/flux/cl:op
Show oidc info
bazel run //infra/flux/cl:oidc
Renew certificate
bazel run //infra/flux/cl:cmctl -- renew traefik-gateway-websecure-tls -n traefik
Update secrets
- 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
Purpose
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.
Requirements
Requirement: Dedicated K3s management host
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.
Scenario: Inspect the management cluster deployment definition
- 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
Requirement: Git reconciliation for Flux and Harbor
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.
Scenario: Resolve the reconciliation source 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
Requirement: Packaged operator and controller configuration
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
Purpose
Define Flux DNS management through the owner’s tf_setup root,
including canonical declarations, scoped credentials, and offline source checks
before adopting live records.
Requirements
Requirement: Owner-local DNS configuration
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.
Scenario: Inspect the preparatory configuration
- 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
Scenario: Inspect the adopted configuration
- 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
Scenario: Adopt existing records
- 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
Scenario: Reconcile unchanged declarations
- 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
Requirement: Scoped DNS execution and offline checks
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
Links
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
Links
Repository configuration
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.
Deployment
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
Purpose
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.
Requirements
Requirement: Scoped Xen Orchestra provisioning and disk checks
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.
Scenario: Detect unexpected Xen disk attachments
- 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
Requirement: Canonical service routing with legacy hostname support
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.
Scenario: Inspect support for existing Forgejo clients
- 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
Requirement: Guarded Vault OIDC bootstrap
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.
Scenario: Preserve an existing active authentication source
- WHEN Forgejo reports exactly one active OAuth2 source named
vault
- THEN the bootstrap accepts it and skips source creation
Scenario: Reject an ambiguous authentication source
- 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
Requirement: Vault identities and service grants with catalog named roles
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.
Scenario: Reject unsupported group membership
- 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
Scenario: Resolve a catalog member through Vault
- 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
Scenario: Retain service access while restricting a named developer
- 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
Purpose
Define Forgejo DNS management through the owner’s tf_setup root,
including canonical declarations, scoped credentials, and offline source checks
before adopting live records.
Requirements
Requirement: Owner-local DNS configuration
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.
Scenario: Inspect the preparatory configuration
- 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
Scenario: Inspect the adopted configuration
- 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
Scenario: Adopt existing records
- 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
Scenario: Reconcile unchanged declarations
- 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
Requirement: Scoped DNS execution and offline checks
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
Deploy VMs
bazel run //infra/forgejo_runner/tf_setup # Create VMs
bazel run //infra/forgejo_runner/ansible # Configure VMs
7.1 -
Forgejo runner infrastructure specification
Purpose
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.
Requirements
Requirement: Dedicated runner VM allocation
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.
Scenario: Inspect the runner capacity 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
Requirement: Explicit host-only deployment baseline
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.
Scenario: Determine the effect of the current playbook
- 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
Requirement: Packaged setup and deployment configuration
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.
Scenario: Resolve the registration credential source
- 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
Purpose
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.
Requirements
Requirement: Owner-local DNS configuration
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.
Scenario: Inspect the preparatory configuration
- 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
Scenario: Inspect the adopted configuration
- 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
Scenario: Adopt existing records
- 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
Scenario: Reconcile unchanged declarations
- 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
Requirement: Scoped DNS execution and offline checks
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
Purpose
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.
Requirements
Requirement: Consume the shared organization and repository catalog
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.
Scenario: Consume a catalog repository
- WHEN the package renders its repository resources
- THEN every catalog GitHub repository is managed from its catalog record
Requirement: Publish project landings from the apex site
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.
Scenario: Publish a project landing
- 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
Requirement: Adopt existing resources without recreating them
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.
Scenario: An existing repository cannot be imported
- WHEN import cannot resolve an observed catalog repository
- THEN the operation fails rather than creating a replacement repository
Requirement: Restrict developers to contribution branches
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.
Scenario: A developer contributes a change
- 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
Scenario: Publish the main site
- 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
Requirement: Keep repository provisioning separate from site and DNS publication
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.
Scenario: Review a site rollout
- 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
Requirement: Provide an explicit certificate reprovision procedure
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.
Scenario: Reprovision the apex site certificate
- 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.
Adoption and access
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.
Publish the main site
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.
Import and apply
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:
- 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.
- 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
Links
CE Login
- Go to User Profile, copy CLI secret
- Run:
podman login harbor.alwaldend.com
Update secrets
- 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
Purpose
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.
Requirements
Requirement: Dedicated K3s registry host
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.
Scenario: Inspect management API access
- 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
Requirement: Remote Flux chart reconciliation
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.
Scenario: Inspect the Harbor release declaration
- 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
Requirement: Vault-backed registry identity configuration
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.
Scenario: Resolve OIDC identity settings
- 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
Requirement: Registry projects and guest access
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.
Scenario: Inspect registry consumer permissions
- 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
Purpose
Define Harbor DNS management through the owner’s tf_setup root,
including canonical declarations, scoped credentials, and offline source checks
before adopting live records.
Requirements
Requirement: Owner-local DNS configuration
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.
Scenario: Inspect the preparatory configuration
- 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
Scenario: Inspect the adopted default
- 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
Scenario: Adopt existing records
- 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
Scenario: Reconcile unchanged declarations
- 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
Requirement: Scoped DNS execution and offline checks
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
Run ansible
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
Update signed image url
Recreate VMs
bazel run infra/ingress/tf -- -replace 'yandex_compute_disk.vpc["*"]'
Regenerate wireguard keys
- 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
Purpose
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.
Requirements
Requirement: Provision ingress hosts from the declared host map
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.
Scenario: Evaluate the baseline host configuration
- 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
Requirement: Connect declared hosts through WireGuard
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
Requirement: Route to backend addresses that do not return to ingress
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.
Scenario: Add a service behind ingress
- 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
Requirement: Deploy Traefik with HTTPS and client-certificate authentication
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.
Scenario: Render the ingress TLS configuration
- 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
Purpose
Define retained ingress DNS ownership through the owner’s tf root,
including canonical declarations, scoped execution, and offline source checks.
Requirements
Requirement: Owner-local DNS configuration
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.
Scenario: Inspect the preparatory configuration
- 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
Scenario: Adopt existing DNS records
- 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
Scenario: Inspect adopted source defaults
- 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
Scenario: Reconcile existing DNS records
- WHEN the owning root plans against its adopted state and unchanged declarations
- THEN it proposes no record additions, changes, replacements, or deletions
Requirement: Scoped DNS execution and offline checks
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.
Deployment
- Open Winbox
- Open new terminal
- Run
/export
- Copy output
Links
Exports
Router1
/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
Router2
# 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
Purpose
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.
Requirements
Requirement: Preserve export snapshots as the package’s documented interface
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.
Scenario: Inspect the router package
- 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
Requirement: Describe the two routers according to their recorded topology
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.
Scenario: Compare the recorded network roles
- 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
Requirement: Retain the recorded router1 traffic-policy structure
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.
Scenario: Review router1’s recorded IPv4 policy
- 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
Requirement: Supply site-local DNS records to the DNS owner
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.
Scenario: Assemble the dc1 DNS configuration
- 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
Purpose
Define MikroTik router DNS management through the owner’s tf root,
including canonical declarations, scoped credentials, and offline source checks
before adopting live records.
Requirements
Requirement: Owner-local DNS configuration
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.
Scenario: Inspect the preparatory configuration
- 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
Scenario: Adopt existing DNS records
- 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
Scenario: Inspect adopted source defaults
- 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
Scenario: Reconcile existing DNS records
- WHEN the owning root plans against its adopted state and unchanged declarations
- THEN it proposes no record additions, changes, replacements, or deletions
Requirement: Scoped DNS execution and offline checks
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 -
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.
Links
13.1 -
NAS infrastructure Specification
Purpose
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.
Requirements
Requirement: Scoped NAS configuration packaging
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.
Scenario: Inspect the NAS configuration and Vault packaging
- 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
Requirement: Distinct local and global cloud DNS destinations
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.
Scenario: Consume DNS declarations for different views
- 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
Purpose
Define NAS DNS management through the owner’s tf root,
including canonical declarations, scoped credentials, and offline source checks
before adopting live records.
Requirements
Requirement: Owner-local DNS configuration
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.
Scenario: Inspect the preparatory configuration
- 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
Scenario: Adopt existing DNS records
- 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
Scenario: Inspect adopted source defaults
- 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
Scenario: Reconcile existing DNS records
- WHEN the owning root plans against its adopted state and unchanged declarations
- THEN it proposes no record additions, changes, replacements, or deletions
Requirement: Scoped DNS execution and offline checks
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 -
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.
Layout
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.
Traefik and TLS
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.
Secrets
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"
Deployment
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
Purpose
Define OpenHands DNS management through the owner’s tf_setup root,
including canonical declarations, scoped credentials, and offline source checks
before adopting live records.
Requirements
Requirement: Owner-local DNS configuration
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.
Scenario: Inspect the preparatory configuration
- 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
Scenario: Inspect the adopted default
- 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
Scenario: Adopt existing records
- 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
Scenario: Reconcile without changes after adoption
- 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
Scenario: Provision missing declarations
- 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
Requirement: Scoped DNS execution and offline checks
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
Links
Deployment
bazel run //infra/pve/ansible # host setup
bazel run //infra/pve/tf/tf.apply # tf setup
Recreate the test VM
bazel run //infra/pve/tf:tf.apply -- --replace module.vm_cloudinit_test.proxmox_vm_qemu.vm
Cloud-init snippet update
bazel run //infra/pve/ansible -- --tags pve_snippets
Update ACME account
Pve token
- 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
Purpose
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.
Requirements
Requirement: AppRole resource pools
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.
Scenario: Declare pools for known AppRoles
- 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.
Requirement: Packaged host and snippet configuration
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.
Scenario: Assemble the PVE deployment package
- 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.
Requirement: Cloud-init test virtual machine
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.
Scenario: Resolve the test VM’s desired 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
Purpose
Define Proxmox cluster DNS management through the owner’s tf root,
including canonical declarations, scoped credentials, and offline source checks
before adopting live records.
Requirements
Requirement: Owner-local DNS configuration
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.
Scenario: Inspect the preparatory configuration
- 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
Scenario: Adopt existing DNS records
- 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
Scenario: Inspect adopted source defaults
- 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
Scenario: Reconcile existing DNS records
- WHEN the owning root plans against its adopted state and unchanged declarations
- THEN it proposes no record additions, changes, replacements, or deletions
Requirement: Scoped DNS execution and offline checks
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
Purpose
Define shared organization, repository identity, naming, and named access
configuration for the GitHub, GitLab, and Forgejo infrastructure consumers.
Requirements
Requirement: Authoritative organization and repository inventory
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.
Scenario: Consume one organization definition
- 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
Requirement: Stable names follow ownership and original upstream
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.
Scenario: Copy an organization-owned repository
- WHEN the first-party
alwaldend/src repository is copied from GitHub to
GitLab or Forgejo
- THEN its destination remains
alwaldend/src
Scenario: Fork an external repository
- 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
Requirement: Named developer access excludes protected default writes
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.
Scenario: Develop on a feature branch
- 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
Requirement: Adopt existing resources without recreation
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.
Scenario: Adopt an existing repository
- 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
Scenario: Encounter a destructive plan
- 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
Requirement: GitLab copies and fork preserve their source identity
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.
Scenario: Complete the initial GitLab population
- 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
Requirement: Adopt GitLab default-branch protections without removal
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.
Scenario: Adopt an automatically protected default branch
- 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.
Run OpenSpec
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.
Maintain work
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.
Goal migration
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.
Record mapping
Mapping and acceptance
- 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.
Provenance and navigation
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
Purpose
Keep tracked go.mod files on the Go version configured for
tools/go_mod.
Sources: tool README,
tool BUILD, and
archive.
Requirements
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.
Scenario: Update module files
- 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
Scenario: Check module files
- 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
Requirement: Repository quality integration
The go.mod version check MUST run as part of //:repo_quality_test so a
version mismatch fails repository quality.
Scenario: Quality suite covers the check
- WHEN a
go.mod file’s go directive differs from the configured
version
- THEN
//:repo_quality_test fails
17.1.3 -
Repository Evolution Specification
Purpose
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.
Requirements
Requirement: Component ownership and source boundaries
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.
Scenario: Select an implementation owner
- 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
Requirement: Reproducible Bazel development
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.
Scenario: Change a shared dependency
- 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
Scenario: Build a nested module standalone
- 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
Scenario: Expand root targets across a nested boundary
- 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
Requirement: Isolated implementation and evidence
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.
Scenario: Finish a repository change
- 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
Requirement: Declarative infrastructure authority
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.
Scenario: Validate an infrastructure specification
- 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
Requirement: OpenSpec coverage and work state
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.
Scenario: Change a component contract
- 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
Scenario: Evolve the repository itself
- 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
Scenario: Resume unfinished maintained work
- 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
Scenario: Validate a standalone project
- 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
Requirement: Legacy goal migration and deprecation
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.
Scenario: Inspect a migrated historical record
- 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
Requirement: Rule scripts and generated documentation
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.
Scenario: A rule needs to run scripted logic
- 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
Scenario: A rule set exposes public rules
- 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.
Scenario: Consume a relocated rule module
- 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
Scenario: Discover a relocated owner’s specifications and rules
- 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.
Scenario: Build or import the remaining repository catalog
- 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
Scenario: Retire a selected landing site
- 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
Links
Fetch and fix subs for a particular subscription id
bazel run //infra/threexui:fix_subs -- --hosts njalla1.nodes.threexui.alwaldend.com,yc1.nodes.threexui.alwaldend.com --sub_id subid
Fix subs from a local file
bazel run //infra/threexui:fix_subs -- --sub_file path_to_file
18.1 -
Infrastructure 3x-ui
Purpose
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.
Requirements
Requirement: Separate provisioning, deployment, and service configuration
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.
Scenario: Select a 3x-ui workflow
- 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
Requirement: Terminate proxy traffic at the declared local services
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.
Scenario: Render a subscription route
- 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
Requirement: Declare explicit node and outbound routing relationships
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
Purpose
Define retained 3x-ui DNS ownership through the owner’s tf_setup root,
including canonical declarations, scoped execution, and offline source checks.
Requirements
Requirement: Owner-local DNS configuration
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.
Scenario: Inspect the preparatory configuration
- 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
Scenario: Inspect adopted source defaults
- 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
Scenario: Adopt existing DNS records
- 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
Scenario: Reconcile adopted DNS records
- 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
Requirement: Scoped DNS execution and offline checks
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
Links
Deployment
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
Backup
bazel run //infra/vault:backup
Unseal
With a working Vault:
bazel run //infra/vault:unseal
Without a working Vault:
bazel run //infra/vault:unseal_standalone
Fix quorum
bazel run //infra/vault/ansible:fix_quorum
Set up only VMs
bazel run //infra/vault/ansible:ansible.vm # Set up only VMs
bazel run //infra/vault/ansible:ansible.bm # Set up only bare metal
Tf
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
Replace VMs
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
Generate and import a user cilent certificate
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"
Generate a host cilent certificate
bazel run //infra/vault:gen_client_cert -- --host some-host --output_dir "${HOME}/.al/client_cert"
Generate a user device cilent certificate
bazel run //infra/vault:gen_client_cert -- --host some-host --user username --output_dir "${HOME}/.al/client_cert"
Unseal
- Prepare encrypted unseal token
- Run and input the encrypted token:
bazel run //infra/vault:unseal
Root token
- 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
Generate an EAB for ACME
bazel run //infra/vault -- write -f pki/ica_servers/roles/ica_servers_dc1_pve1/acme/new-eab
Sign a client ssh key
bazel run //:vault -- write ssh/clients/sign/admins ttl=30000000 public_key=@"${HOME}/.ssh/key"
Revoke all tokens
bazel run //infra/vault -- token revoke -mode=path auth
Vault certificates
Vault certificates (tls_cert_file, tls_key_file) should be updated manually
Read OIDC client info
bazel run //infra/vault -- read identity/oidc/client/src_infra_dc1_forgejo1_provider
Read all entity aliases
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
Purpose
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.
Requirements
Requirement: Separate host provisioning from Vault configuration
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.
Scenario: Prepare an authorized host setup
- 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.
Scenario: Render a Vault host configuration
- 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
Requirement: Declare authentication and secret-engine boundaries
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
Requirement: Package recovery and certificate operations explicitly
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.
Scenario: Select the standalone unseal workflow
- 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
Requirement: Give each DNS owner a component identity
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.
Scenario: Introduce a landing site’s DNS stage
- 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
Scenario: Add host DNS management
- WHEN host_bot uses its dedicated DNS AppRole
- THEN its existing Ansible authentication remains unchanged
Requirement: Restrict provider secret reads to owned DNS views
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
Purpose
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.
Requirements
Requirement: Owner-local DNS configuration
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.
Scenario: Inspect the preparatory configuration
- 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
Scenario: Adopt existing DNS records
- 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
Scenario: Inspect adopted source defaults
- 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
Scenario: Reconcile existing DNS records
- WHEN the owning root plans against its adopted state and unchanged declarations
- THEN it proposes no record additions, changes, replacements, or deletions
Requirement: Scoped DNS execution and offline checks
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
DNS identities
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.
OIDC and certificates
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.
Commands
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
Purpose
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.
Requirements
Requirement: AppRole resource sets with named inventory
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.
Scenario: An AppRole has no assigned inventory
- 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.
Requirement: Immutable OIDC subjects and explicit existing-VM ownership
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.
Scenario: A new AppRole has not logged in to XO
- 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.
Scenario: An existing VM name does not identify a unique owned VM
- WHEN hostname and AppRole tag matching in the named pool returns zero or multiple VMs
- THEN Terraform rejects the existing-VM ownership configuration.
Requirement: Invocation-scoped tenant authentication
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.
Scenario: A component finishes its authenticated invocation
- WHEN normal plugin shutdown follows a successful XO login
- THEN the plugin revokes its issued session using
token.deleteOwn
without revoking unrelated sessions.
Requirement: Declarative certificate issuance and renewal
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.
Scenario: Renew an XCP-ng host certificate
- 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
Purpose
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.
Requirements
Requirement: Owner-local DNS configuration
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.
Scenario: Inspect the preparatory configuration
- 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
Scenario: Adopt existing DNS records
- 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
Scenario: Inspect adopted source defaults
- 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
Scenario: Reconcile existing DNS records
- WHEN the owning root plans against its adopted state and unchanged declarations
- THEN it proposes no record additions, changes, replacements, or deletions
Requirement: Scoped DNS execution and offline checks
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
Scenario: Review and apply only DNS resources
- 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.
XCP-ng host certificates
//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.
OpenID Connect plugin
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)
Links
21.1 -
Infrastructure Yandex Cloud
Purpose
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.
Requirements
Requirement: Keep organization configuration under its owner
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
Requirement: Derive folders from Vault AppRole membership
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.
Scenario: Map an AppRole identity to a cloud folder
- 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
Links
21.2.1 - Tf
Terraform config