diff --git a/.gitignore b/.gitignore index 1c5cea2..f361c46 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,7 @@ # Local operator context for Codex; never commit machine-specific memory. .codex/LOCAL_MEMORY.md + +# Local Python/Ansible control environment and runtime artifacts. +.venv/ +.ansible/ +*.retry diff --git a/README.md b/README.md index 3a9b330..e99e983 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ Die drei vorgesehenen VPN-Netzwerkrollen sind **Familie** (gesamtes Heimnetz), * | Bereich | Belegter Stand | Nächster Nachweis oder Schritt | | --- | --- | --- | -| KI-Server | Debian-Basissystem und getrennte SSH-Konten; lesender Teilabgleich am 18.09.2026 | LLM-Anwendungsstack später mit Ansible einrichten und abnehmen | +| KI-Server | Debian-Basissystem, lesender Preflight, verifizierter `ansible-deploy`-Account und idempotent angewendete minimale Host-Baseline | Podman-Runtime als nächste getrennte Rolle planen und kontrolliert abnehmen | | VPN | wg-easy v15.4.0 auf CT 105 aktiv; ein externer Laptop-Teiltest liegt vor | Rollenrechte serverseitig umsetzen und mit erlaubten sowie gesperrten Zielen testen | | Reverse Proxy | NPM auf CT 103 dokumentiert; Fail2Ban laut Betreiber fest gepinnt und wieder in Betrieb | Effektive Jails, Sperren und Wiederanlauf prüfen | | Identität | Keycloak/OIDC laut Betreiber produktiv für Vaultwarden | Öffentlich erreichbare Keycloak-Administration sperren und den OIDC-Login erneut prüfen | @@ -46,10 +46,12 @@ Der [Dokumentationsindex](docs/GNAI-000-DOC_Index.md) enthält die datierten Que | [Client-Verwaltung](docs/GNAI-006-SEC_VPN_Client-Management.md) | Profile ausgeben, ändern, widerrufen und Rollen prüfen | | [Windows 11](docs/GNAI-006-SEC_VPN_Clients_Windows11.md) und [Linux](docs/GNAI-006-SEC_VPN_Clients_Linux.md) | Einrichtung und Nutzung auf Endgeräten | | [WireGuard-Implementation-Log](docs/GNAI-006-SEC_VPN_Implementation-Log.md) | Historie, Tests und Korrekturen früherer Befunde | +| [Ansible-Deployment-Planung](docs/GNAI-007-PLAN_Ansible-Deployment.md) | nicht finaler Arbeitsentwurf zur reproduzierbaren Bereitstellung, Gitea-Integration und geplanten Folgedokumentation | +| [Ansible-Control-Projekt](ansible/README.md) | lokale Einrichtung, lesender Server-Preflight, Deployment-Account und gemeinsame Host-Baseline | ## Arbeiten mit dem Repository -Dieses Repository enthält derzeit Dokumentation und noch keine Ansible-Rollen oder ausführbare Bereitstellung des KI-Anwendungsstacks. Vor einer Infrastrukturänderung den Ist-Stand am betroffenen Host prüfen, die passende Fachanleitung lesen und die dort genannten Abnahmeschritte dokumentieren. Der KI-Server soll bis zum geplanten Ansible-Sprint abgesehen von den eingerichteten Benutzerzugängen ein Basissystem bleiben. +Dieses Repository enthält Dokumentation und ein Ansible-Control-Projekt. Es prüft Inventar, SSH-Zugang, Hostidentität, Betriebssystem und die dokumentierten GPUs. Eine erste gemeinsame Rolle verwaltet ausschließlich die Konfigurations- und Datenwurzeln für Production und Testing; der KI-Anwendungsstack ist noch nicht bereitgestellt. Vor einer Infrastrukturänderung den Ist-Stand am betroffenen Host prüfen, die passende Fachanleitung lesen und die dort genannten Abnahmeschritte dokumentieren. WireGuard-Profile enthalten private Schlüssel. Sie gehören nicht in Git, Ausgaben oder freigegebene Dokumente. Das lokale Verzeichnis `docs/GNAI-006-profiles/` schließt Profil-Dateien über eine eigene `.gitignore` aus; es ist kein Verteiler- oder Backup-Ort für Zugangsdaten. diff --git a/ansible/README.md b/ansible/README.md new file mode 100644 index 0000000..73b13ac --- /dev/null +++ b/ansible/README.md @@ -0,0 +1,140 @@ +# Ansible control project + +This directory contains the reproducible control project for `ghostnet-ki`. +The read-only `playbooks/preflight.yml` verifies the documented host identity +and hardware. The first writing milestone, `playbooks/host.yml`, applies a +small shared host baseline through the dedicated deployment account. + +## Control-node setup + +Use Python 3.12 through 3.14 and create a virtual environment at the repository +root: + +```bash +python3 -m venv .venv +.venv/bin/python -m pip install --upgrade pip +.venv/bin/python -m pip install -r ansible/requirements-control.txt +``` + +Load the private key for the read-only `ghost-agent` account into an SSH agent. +Do not store private keys or passphrases in this repository: + +```bash +ssh-add /path/to/ghost-agent-private-key +``` + +The workstation must already trust the verified SSH host key for `10.0.0.90`. +An unexpected host-key change must be checked through a trusted console before +continuing. + +## Read-only preflight + +Run the commands from the `ansible/` directory: + +```bash +cd ansible +../.venv/bin/ansible-inventory --graph +../.venv/bin/ansible-playbook playbooks/preflight.yml --syntax-check +../.venv/bin/ansible-playbook playbooks/preflight.yml +``` + +The final recap must report `changed=0` and `failed=0`. The playbook verifies: + +- hostname `ghostnet-ki`; +- Debian major version 13; +- default IPv4 address `10.0.0.90`; +- remote Python availability; and +- two AMD Navi 21 PCI devices with ID `1002:73bf`. + +The inventory deliberately contains no private-key path. Each operator supplies +their authorized key through `ssh-agent` or a local Ansible override. + +## One-time deployment-account bootstrap + +Status 2026-09-19: The initial bootstrap has been applied to `ghostnet-ki`, and +the separate verification playbook confirmed SSH access and non-interactive +effective UID 0 with `changed=0` and `failed=0`. Run the bootstrap again only +for an intentional repair, rotation or idempotence check. + +The bootstrap creates the dedicated `ansible-deploy` account. Its password is +locked, its committed public key is restricted to the LAN and WireGuard source +networks, and `/etc/sudoers.d/90-ansible-deploy` is validated with `visudo` +before installation. The account receives `NOPASSWD` root escalation because +the planned host, package, systemd and container management cannot be expressed +as a small stable command allowlist. + +The matching private key stays outside this repository. On the workstation on +which the initial key was created, it is stored as: + +```text +~/.ssh/id_ed25519_ghostnet_ai_ansible_deploy +``` + +The automation key has no interactive passphrase so a future runner can use it +non-interactively. It is protected by filesystem mode `0600`, SSH source-network +restrictions and the dedicated account. Compromise of this private key would +nevertheless grant root-equivalent deployment access; rotate it when moving the +deployment identity to the runner or after any suspected disclosure. + +Load the existing personal administrator key into an SSH agent, then run the +bootstrap interactively from the `ansible/` directory. Ansible will ask for the +personal administrator's existing sudo password: + +```bash +ssh-add /path/to/personal-ghost-private-key +../.venv/bin/ansible-playbook \ + --inventory inventories/bootstrap/hosts.yml \ + playbooks/bootstrap-deploy-account.yml \ + --ask-become-pass +``` + +Do not send the password through chat, store it in Git, or place it in a shell +history entry. The bootstrap is idempotent; a second run should report no +changes. + +After a successful bootstrap, verify the new account independently: + +```bash +../.venv/bin/ansible-playbook \ + playbooks/verify-deploy-access.yml \ + --extra-vars ansible_user=ansible-deploy \ + --private-key ~/.ssh/id_ed25519_ghostnet_ai_ansible_deploy +``` + +The verification must report `changed=0` and `failed=0`. It confirms both the +unprivileged SSH identity and non-interactive escalation to effective UID 0. + +## Shared host baseline + +The host baseline is common to Production and Testing. It verifies the target +identity and at least 100 GiB free on the root filesystem, then manages these +root-owned directory roots: + +```text +/etc/ghostnet-ai/{production,testing} +/var/lib/ghostnet-ai/{production,testing,shared} +``` + +It does not install software or start services. Use the dedicated host +inventory and supply the private deployment key outside the repository: + +```bash +../.venv/bin/ansible-playbook \ + --inventory inventories/host/hosts.yml \ + playbooks/host.yml \ + --syntax-check + +../.venv/bin/ansible-playbook \ + --inventory inventories/host/hosts.yml \ + playbooks/host.yml \ + --check --diff \ + --private-key ~/.ssh/id_ed25519_ghostnet_ai_ansible_deploy + +../.venv/bin/ansible-playbook \ + --inventory inventories/host/hosts.yml \ + playbooks/host.yml \ + --private-key ~/.ssh/id_ed25519_ghostnet_ai_ansible_deploy +``` + +Run the final command a second time. The second recap must report `changed=0` +and `failed=0`; otherwise the role has not demonstrated convergence. diff --git a/ansible/ansible.cfg b/ansible/ansible.cfg new file mode 100644 index 0000000..4f9d2d6 --- /dev/null +++ b/ansible/ansible.cfg @@ -0,0 +1,13 @@ +[defaults] +inventory = inventories/testing/hosts.yml +home = .ansible +host_key_checking = True +interpreter_python = auto_silent +local_tmp = .ansible/tmp +retry_files_enabled = False +roles_path = roles +timeout = 15 + +[ssh_connection] +pipelining = True +ssh_args = -o ControlMaster=auto -o ControlPersist=60s diff --git a/ansible/files/ssh/ansible-deploy.pub b/ansible/files/ssh/ansible-deploy.pub new file mode 100644 index 0000000..e58a1f1 --- /dev/null +++ b/ansible/files/ssh/ansible-deploy.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJE3UGGHyB+CMA2sqPpI/6efn9u3eLelL2MkHqdt2naB ansible-deploy@ghostnet-control-2026-09 diff --git a/ansible/inventories/bootstrap/hosts.yml b/ansible/inventories/bootstrap/hosts.yml new file mode 100644 index 0000000..1d46c36 --- /dev/null +++ b/ansible/inventories/bootstrap/hosts.yml @@ -0,0 +1,8 @@ +--- +all: + children: + llm_servers: + hosts: + ghostnet-ki: + ansible_host: 10.0.0.90 + ansible_user: ghost diff --git a/ansible/inventories/host/hosts.yml b/ansible/inventories/host/hosts.yml new file mode 100644 index 0000000..cd0993d --- /dev/null +++ b/ansible/inventories/host/hosts.yml @@ -0,0 +1,8 @@ +--- +all: + children: + llm_servers: + hosts: + ghostnet-ki: + ansible_host: 10.0.0.90 + ansible_user: ansible-deploy diff --git a/ansible/inventories/testing/hosts.yml b/ansible/inventories/testing/hosts.yml new file mode 100644 index 0000000..c27f925 --- /dev/null +++ b/ansible/inventories/testing/hosts.yml @@ -0,0 +1,8 @@ +--- +all: + children: + llm_servers: + hosts: + ghostnet-ki: + ansible_host: 10.0.0.90 + ansible_user: ghost-agent diff --git a/ansible/playbooks/bootstrap-deploy-account.yml b/ansible/playbooks/bootstrap-deploy-account.yml new file mode 100644 index 0000000..ef09c9f --- /dev/null +++ b/ansible/playbooks/bootstrap-deploy-account.yml @@ -0,0 +1,98 @@ +--- +- name: Bootstrap the dedicated Ansible deployment account + hosts: llm_servers + become: true + gather_facts: true + + vars: + ansible_deploy_user: ansible-deploy + ansible_deploy_public_key_file: >- + {{ playbook_dir }}/../files/ssh/ansible-deploy.pub + ansible_deploy_ssh_sources: + - 10.0.0.0/24 + - 10.8.0.0/24 + + pre_tasks: + - name: Refuse to bootstrap through the target deployment account + ansible.builtin.assert: + that: + - ansible_user != ansible_deploy_user + fail_msg: >- + Run this one-time bootstrap through an existing personal administrator + account, not through {{ ansible_deploy_user }}. + + - name: Verify the local deployment public key + ansible.builtin.assert: + that: + - lookup("ansible.builtin.file", ansible_deploy_public_key_file) is match("^ssh-ed25519 ") + fail_msg: >- + The expected Ed25519 public key is missing or malformed at + {{ ansible_deploy_public_key_file }}. + + tasks: + - name: Create the dedicated deployment account with a locked password + ansible.builtin.user: + name: "{{ ansible_deploy_user }}" + comment: Ghostnet AI Ansible deployment account + create_home: true + home: "/home/{{ ansible_deploy_user }}" + password_lock: true + shell: /bin/bash + state: present + + - name: Protect the deployment account home directory + ansible.builtin.file: + path: "/home/{{ ansible_deploy_user }}" + state: directory + owner: "{{ ansible_deploy_user }}" + group: "{{ ansible_deploy_user }}" + mode: "0750" + + - name: Create the deployment SSH directory + ansible.builtin.file: + path: "/home/{{ ansible_deploy_user }}/.ssh" + state: directory + owner: "{{ ansible_deploy_user }}" + group: "{{ ansible_deploy_user }}" + mode: "0700" + + - name: Install the restricted deployment public key + ansible.builtin.copy: + content: | + restrict,from="{{ ansible_deploy_ssh_sources | join(',') }}" {{ lookup('ansible.builtin.file', ansible_deploy_public_key_file) | trim }} + dest: "/home/{{ ansible_deploy_user }}/.ssh/authorized_keys" + owner: "{{ ansible_deploy_user }}" + group: "{{ ansible_deploy_user }}" + mode: "0600" + + - name: Grant explicit non-interactive root escalation for Ansible + ansible.builtin.copy: + content: | + # Managed by Ghostnet AI Ansible bootstrap. + {{ ansible_deploy_user }} ALL=(root) NOPASSWD: ALL + dest: "/etc/sudoers.d/90-{{ ansible_deploy_user }}" + owner: root + group: root + mode: "0440" + validate: /usr/sbin/visudo -cf %s + + - name: Read the effective deployment sudo policy + ansible.builtin.command: + argv: + - /usr/bin/sudo + - -l + - -U + - "{{ ansible_deploy_user }}" + changed_when: false + register: ansible_deploy_sudo_policy + + - name: Verify non-interactive root escalation is effective + ansible.builtin.assert: + that: + - 'ansible_deploy_sudo_policy.stdout is search("NOPASSWD: ALL")' + fail_msg: >- + The deployment account exists, but its validated NOPASSWD sudo policy + is not effective. + success_msg: >- + The deployment account and its validated sudo policy are ready for + the separate access verification playbook. diff --git a/ansible/playbooks/host.yml b/ansible/playbooks/host.yml new file mode 100644 index 0000000..451e89c --- /dev/null +++ b/ansible/playbooks/host.yml @@ -0,0 +1,12 @@ +--- +- name: Apply the shared Ghostnet AI host baseline + hosts: llm_servers + become: true + gather_facts: true + gather_subset: + - min + - network + - hardware + + roles: + - role: host_baseline diff --git a/ansible/playbooks/preflight.yml b/ansible/playbooks/preflight.yml new file mode 100644 index 0000000..809729a --- /dev/null +++ b/ansible/playbooks/preflight.yml @@ -0,0 +1,60 @@ +--- +- name: Verify read-only access to the Ghostnet AI server + hosts: llm_servers + become: false + gather_facts: true + gather_subset: + - min + - hardware + - network + + tasks: + - name: Verify the expected host identity and operating system + ansible.builtin.assert: + that: + - ansible_facts["hostname"] == "ghostnet-ki" + - ansible_facts["distribution"] == "Debian" + - ansible_facts["distribution_major_version"] == "13" + - ansible_facts["default_ipv4"]["address"] == "10.0.0.90" + - ansible_facts["python"]["version"]["major"] >= 3 + fail_msg: >- + The connected host does not match the documented Ghostnet AI server + baseline. Stop before using any deployment playbook. + success_msg: The expected Debian 13 host ghostnet-ki is connected. + + - name: Read the expected AMD GPU PCI devices + ansible.builtin.command: + argv: + - lspci + - -D + - -nn + - -d + - "1002:73bf" + check_mode: false + changed_when: false + register: preflight_gpu_devices + + - name: Verify both documented AMD GPUs are present + ansible.builtin.assert: + that: + - preflight_gpu_devices.stdout_lines | length == 2 + fail_msg: >- + Expected two AMD Navi 21 PCI devices with ID 1002:73bf, but found + {{ preflight_gpu_devices.stdout_lines | length }}. + success_msg: Both documented AMD Navi 21 PCI devices are present. + + - name: Report the verified read-only baseline + ansible.builtin.debug: + msg: + hostname: "{{ ansible_facts['hostname'] }}" + operating_system: >- + {{ ansible_facts['distribution'] }} + {{ ansible_facts['distribution_version'] }} + default_ipv4: "{{ ansible_facts['default_ipv4']['address'] }}" + processor_vcpus: "{{ ansible_facts['processor_vcpus'] }}" + memory_mib: "{{ ansible_facts['memtotal_mb'] }}" + python_version: >- + {{ ansible_facts['python']['version']['major'] }}. + {{- ansible_facts['python']['version']['minor'] }}. + {{- ansible_facts['python']['version']['micro'] }} + amd_gpu_devices: "{{ preflight_gpu_devices.stdout_lines }}" diff --git a/ansible/playbooks/verify-deploy-access.yml b/ansible/playbooks/verify-deploy-access.yml new file mode 100644 index 0000000..0420bfa --- /dev/null +++ b/ansible/playbooks/verify-deploy-access.yml @@ -0,0 +1,41 @@ +--- +- name: Verify the dedicated Ansible deployment account + hosts: llm_servers + become: false + gather_facts: false + + tasks: + - name: Verify Ansible module execution without privilege escalation + ansible.builtin.ping: + + - name: Read the SSH account identity + ansible.builtin.command: + argv: + - /usr/bin/id + - -un + changed_when: false + register: deploy_account_identity + + - name: Verify the expected SSH account + ansible.builtin.assert: + that: + - deploy_account_identity.stdout == "ansible-deploy" + fail_msg: The SSH connection did not use the ansible-deploy account. + + - name: Read the effective identity through sudo + become: true + ansible.builtin.command: + argv: + - /usr/bin/id + - -u + changed_when: false + register: deploy_root_identity + + - name: Verify non-interactive root escalation + ansible.builtin.assert: + that: + - deploy_root_identity.stdout == "0" + fail_msg: Non-interactive sudo did not produce effective UID 0. + success_msg: >- + The dedicated deployment account can connect and escalate privileges + non-interactively. diff --git a/ansible/requirements-control.txt b/ansible/requirements-control.txt new file mode 100644 index 0000000..5e30276 --- /dev/null +++ b/ansible/requirements-control.txt @@ -0,0 +1,2 @@ +# ansible-core 2.21 supports Python 3.12 through 3.14 on the control node. +ansible-core==2.21.4 diff --git a/ansible/roles/host_baseline/README.md b/ansible/roles/host_baseline/README.md new file mode 100644 index 0000000..c7da041 --- /dev/null +++ b/ansible/roles/host_baseline/README.md @@ -0,0 +1,17 @@ +# Shared host baseline + +The `host_baseline` role manages state shared by the Production and Testing +environments on `ghostnet-ki`. + +Its first version deliberately has a narrow scope: + +- refuse changes when hostname, Debian major version or primary IPv4 address + do not match the declared target; +- require at least 100 GiB free on the root filesystem; and +- create root-owned configuration and persistent-data directory roots for both + environments and future shared data. + +It does not install packages, drivers, Podman or applications and does not +start or stop services. Ownership below the directory roots will be assigned +to dedicated service accounts when the corresponding application roles are +implemented. diff --git a/ansible/roles/host_baseline/defaults/main.yml b/ansible/roles/host_baseline/defaults/main.yml new file mode 100644 index 0000000..020eb69 --- /dev/null +++ b/ansible/roles/host_baseline/defaults/main.yml @@ -0,0 +1,24 @@ +--- +host_baseline_expected_hostname: ghostnet-ki +host_baseline_expected_distribution: Debian +host_baseline_expected_distribution_major_version: "13" +host_baseline_expected_ipv4_address: 10.0.0.90 + +# Preserve enough headroom for host operation and controlled model handling. +host_baseline_minimum_root_free_gib: 100 + +host_baseline_directories: + - path: /etc/ghostnet-ai + mode: "0755" + - path: /etc/ghostnet-ai/production + mode: "0750" + - path: /etc/ghostnet-ai/testing + mode: "0750" + - path: /var/lib/ghostnet-ai + mode: "0755" + - path: /var/lib/ghostnet-ai/production + mode: "0750" + - path: /var/lib/ghostnet-ai/testing + mode: "0750" + - path: /var/lib/ghostnet-ai/shared + mode: "0755" diff --git a/ansible/roles/host_baseline/tasks/main.yml b/ansible/roles/host_baseline/tasks/main.yml new file mode 100644 index 0000000..2f611e2 --- /dev/null +++ b/ansible/roles/host_baseline/tasks/main.yml @@ -0,0 +1,59 @@ +--- +- name: Verify the target host before changing shared state + ansible.builtin.assert: + that: + - ansible_facts["hostname"] == host_baseline_expected_hostname + - ansible_facts["distribution"] == host_baseline_expected_distribution + - >- + ansible_facts["distribution_major_version"] == + host_baseline_expected_distribution_major_version + - >- + ansible_facts["default_ipv4"]["address"] == + host_baseline_expected_ipv4_address + fail_msg: >- + The connected host does not match the declared Ghostnet AI host. No + baseline changes may be applied. + success_msg: The expected Debian host ghostnet-ki is connected. + +- name: Select the root filesystem facts + ansible.builtin.set_fact: + host_baseline_root_mount: >- + {{ (ansible_facts["mounts"] + | selectattr("mount", "equalto", "/") + | list + | first) + | default({}) }} + +- name: Verify the root filesystem was discovered + ansible.builtin.assert: + that: + - host_baseline_root_mount | length > 0 + - host_baseline_root_mount["size_available"] is defined + fail_msg: Root filesystem capacity could not be determined. No changes were made. + +- name: Verify minimum free capacity on the shared root filesystem + ansible.builtin.assert: + that: + - >- + host_baseline_root_mount["size_available"] | int >= + host_baseline_minimum_root_free_gib | int * 1024 ** 3 + fail_msg: >- + The root filesystem has only + {{ (host_baseline_root_mount["size_available"] | int / 1024 ** 3) | round(1) }} + GiB free. At least {{ host_baseline_minimum_root_free_gib }} GiB are + required before applying the shared host baseline. + success_msg: >- + The root filesystem has + {{ (host_baseline_root_mount["size_available"] | int / 1024 ** 3) | round(1) }} + GiB free. + +- name: Create the shared configuration and data directory roots + ansible.builtin.file: + path: "{{ item.path }}" + state: directory + owner: root + group: root + mode: "{{ item.mode }}" + loop: "{{ host_baseline_directories }}" + loop_control: + label: "{{ item.path }}" diff --git a/docs/GNAI-000-DOC_Index.md b/docs/GNAI-000-DOC_Index.md index cbf9d13..efdbe4e 100644 --- a/docs/GNAI-000-DOC_Index.md +++ b/docs/GNAI-000-DOC_Index.md @@ -17,6 +17,8 @@ | [GNAI-006-SEC_VPN_Clients_Windows11.md](GNAI-006-SEC_VPN_Clients_Windows11.md) | offizieller Windows-Client, Import und Verwendung; Junior IT | | [GNAI-006-SEC_VPN_Clients_Linux.md](GNAI-006-SEC_VPN_Clients_Linux.md) | Debian, Arch/CachyOS, Fedora/Kinoite; Junior IT | | [GNAI-006-SEC_VPN_Implementation-Log.md](GNAI-006-SEC_VPN_Implementation-Log.md) | historische v14/v15-Umsetzung und Evidenzkorrekturen; Intermediate IT | +| [GNAI-007-PLAN_Ansible-Deployment.md](GNAI-007-PLAN_Ansible-Deployment.md) | nicht finaler Arbeitsentwurf zum Ansible-/Gitea-Deployment und zur geplanten Folgedokumentation; Infrastrukturadministratoren und Mitwirkende | +| [Ansible-Control-Projekt](../ansible/README.md) | Control-Node, Preflight, Deployment-Account und gemeinsame Host-Baseline; Intermediate IT | ## 2. Verbindliche, bestätigte Entscheidungen @@ -32,6 +34,14 @@ Die getrennten Schlüsselzugänge für `ghost` und `ghost-agent` wurden am 19.09 Der KI-Server ist laut Betreiber frisch eingerichtet. Die LLM-Anwendungen sollen erst in einem späteren Sprint mit Ansible installiert werden. Bis dahin beschreiben GNAI-003 und GNAI-004 für diesen Host ein Zielbild; der lesende SSH-Abgleich betraf nur Basisbetrieb und Zugänge. +Der [Ansible-Deployment-Entwurf](GNAI-007-PLAN_Ansible-Deployment.md) persistiert den Planungsstand vom 19.09.2026. Er ist ausdrücklich keine verbindliche Spezifikation und kein Nachweis einer eingerichteten Deploymentkette. Die dort genannten Runner-, Account-, Freigabe- und Secret-Verfahren sind bis zu ihrer Prüfung und formalen Übernahme vorläufig. + +Das erste Ansible-Control-Projekt mit Testing-Inventar und lesendem Preflight wurde am 19.09.2026 angelegt. Der Live-Lauf über `ghost-agent` bestätigte Hostidentität, Debian 13.6, IPv4-Adresse, Python, CPU, RAM und zwei Navi-21-Geräte mit `changed=0`, `unreachable=0` und `failed=0`. Dieser Nachweis umfasst weder privilegierten Zugriff noch eine eingerichtete Production-/Testing-Laufzeitumgebung. + +Der separate Ed25519-Schlüssel mit Fingerprint `SHA256:IKEInbIo8VVI1USg5+a3ypGK8H8FweJiQu+v7KvhQDU` und das Bootstrap-Playbook wurden am 19.09.2026 verwendet, um `ansible-deploy` einzurichten. Der private Schlüssel liegt ausschließlich außerhalb des Repositories. Der Account besitzt ein gesperrtes Passwort, einen auf LAN- und WireGuard-Quellnetze beschränkten Public Key und eine mit `visudo` validierte Regel `NOPASSWD: ALL`. Ein getrenntes Prüfplaybook bestätigte anschließend Anmeldung, Modulausführung und effektive UID 0 über sudo mit `ok=5 changed=0 unreachable=0 failed=0`. + +Die erste schreibende Rolle `host_baseline` wurde am 19.09.2026 nach erfolgreichem Syntax- und Check-Modus-Lauf angewendet. Sie prüft Hostname, Debian-Hauptversion, Standard-IPv4 sowie mindestens 100 GiB freien Speicher und verwaltet ausschließlich `/etc/ghostnet-ai/{production,testing}` und `/var/lib/ghostnet-ai/{production,testing,shared}` mit Root-Eigentum. Der erste reale Lauf meldete `ok=6 changed=1 unreachable=0 failed=0`; der unmittelbar folgende identische Lauf belegte Idempotenz mit `ok=6 changed=0 unreachable=0 failed=0`. Pakete, Dienste und Anwendungen wurden dabei nicht verändert. + ## 3. Sicherheitsbefunde mit Priorität | Befund | dokumentiertes Verfahren / Status | @@ -59,4 +69,10 @@ Bei der Dokumentationskonsolidierung wurden keine Serverkonfigurationen geänder Am 18.09.2026 wurden WireGuard-LXC und KI-Server ergänzend lesend per SSH geprüft. Der KI-Server-Teilabgleich bestätigt Hostname, Konten, Netzkonfiguration, Debian-Version, CPU, RAM, zwei Navi-21-GPUs und SSD-Partitionierung; BIOS, ROCm und die Anwendungsdienste wurden dabei nicht abgenommen. Details stehen in [GNAI-001](GNAI-001-INFRA_BaseSystem.md) und [GNAI-002](GNAI-002-INFRA_Users.md). +Am 19.09.2026 bestätigte ein erster reproduzierbarer Ansible-Preflight über das unprivilegierte Konto `ghost-agent` erneut `ghostnet-ki`, Debian 13.6, `10.0.0.90`, 14 vCPUs, 64.123 MiB RAM, Remote-Python 3.13.5 und zwei PCI-Geräte `1002:73bf`. Der Play Recap lautete `ok=5 changed=0 unreachable=0 failed=0`. Das Playbook verwendete kein `sudo` und nahm keine Serverkonfiguration vor. + +Beim anschließenden Bootstrap wurden Benutzer, Home-/SSH-Verzeichnisse, `authorized_keys` und sudoers-Datei erfolgreich angelegt. Nur die letzte Assertion des ersten Laufs schlug wegen einer als Mapping interpretierten YAML-Bedingung fehl; die Bedingung wurde anschließend als String korrigiert. Da alle schreibenden Tasks vorher erfolgreich waren, bestätigte das unabhängige Prüfplaybook den wirksamen Endzustand ohne weitere Änderung. Ein vollständiger zweiter Bootstrap-Lauf zur Idempotenzprüfung steht noch aus. + +Für die gemeinsame Host-Baseline bestätigte ein vorgelagerter Check-Modus genau sieben neu anzulegende Verzeichnisse und rund 1.677,7 GiB freien Platz auf `/`. Der anschließende reale Lauf legte die Verzeichniswurzeln an. Eine sofortige Wiederholung ergab `changed=0`; die Rolle konvergiert damit für ihren derzeitigen Umfang. Dieser Nachweis umfasst keine Paket-, Runtime-, GPU- oder Anwendungsinstallation. + Die WireGuard-Evidenz bleibt der Teiltest vom 12.09.2026: CachyOS-Laptop von WAN, Handshake, Ping auf Gateway/Proxmox und direkter IPv4-Internetpfad bestanden; KI-Server war ausgeschaltet. Neue Rollen, weitere Clientvarianten, Gateway-GUI-Zugriff, WAN-/IPv6-Isolation, Neustart und Restore sind noch nicht abgenommen. GNAI-006 ist erst nach Klärung der offenen Werte, Umsetzung und dokumentierter Abnahme technisch abgeschlossen. diff --git a/docs/GNAI-007-PLAN_Ansible-Deployment.md b/docs/GNAI-007-PLAN_Ansible-Deployment.md new file mode 100644 index 0000000..f1fa574 --- /dev/null +++ b/docs/GNAI-007-PLAN_Ansible-Deployment.md @@ -0,0 +1,711 @@ +# Arbeitsentwurf + +**Dokumenten-ID:** GNAI-007-PLAN +**Titel:** Ansible-Deployment für den KI-Server – Planungsstand +**Stand:** 2026-09-19 +**Status:** Nicht finaler Arbeitsentwurf zur Persistierung des aktuellen Planungsstands +**Referenzen:** [Projektstand](GNAI-000-DOC_Index.md), [Zielarchitektur](GNAI-003-INFRA_Architecture.md), [Zugriffskonzept](GNAI-004-SEC_Access-Control.md) + +> **Abgrenzung:** Dieses Dokument hält die Ergebnisse der Planungssession vom +> 19.09.2026 fest. Es ist weder eine freigegebene technische Spezifikation noch +> eine Installationsanleitung oder ein Nachweis über bereits umgesetzte +> Komponenten. Entscheidungen, die noch geprüft oder bestätigt werden müssen, +> sind ausdrücklich als vorläufig oder offen gekennzeichnet. + +--- + +## Änderungsverlauf + +| Datum | Version | Autor | Beschreibung | +| --- | --- | --- | --- | +| 2026-09-19 | 0.1 | spektr / Codex | Planungsstand zu Ansible, Gitea Actions, Runner, Freigaben und künftiger Dokumentation aus der Session festgehalten | +| 2026-09-19 | 0.2 | spektr / Codex | vorläufige Ein-Repository-Strategie sowie logisch getrennte Production- und Testing-Umgebungen auf dem KI-Server ergänzt | +| 2026-09-19 | 0.3 | spektr / Codex | exklusiven zeitlich gestaffelten Betrieb von Production und Testing ohne physische Ressourcenaufteilung als Planungsentscheidung übernommen | +| 2026-09-19 | 0.4 | spektr / Codex | erstes Ansible-Control-Projekt und erfolgreich ausgeführten lesenden Live-Preflight als belegten Implementierungsstand ergänzt | +| 2026-09-19 | 0.5 | spektr / Codex | technischen Schlüssel sowie Bootstrap- und Prüfplaybook für `ansible-deploy` vorbereitet; Ausführung bleibt interaktiv offen | +| 2026-09-19 | 0.6 | spektr / Codex | `ansible-deploy` auf dem KI-Server eingerichtet und SSH-/sudo-Zugriff unabhängig erfolgreich verifiziert | +| 2026-09-19 | 0.7 | spektr / Codex | minimale gemeinsame Host-Baseline mit Schutzprüfungen und getrennten Verzeichniswurzeln implementiert, angewendet und auf Idempotenz geprüft | + +--- + +## 1. Zweck + +Der KI-Server `ghostnet-ki` soll reproduzierbar mit Ansible aufgebaut und +aktualisiert werden. Mehrere berechtigte Personen sollen Änderungen gemeinsam +über Gitea entwickeln, prüfen, freigeben und nachvollziehbar ausrollen können. + +Gesucht wird ein Verfahren, das: + +- den vollständigen Sollzustand des Servers versioniert, +- wiederholbare und möglichst idempotente Deployments ermöglicht, +- Änderungen vor der Anwendung prüft, +- einen nachvollziehbaren Freigabeweg für mehrere Mitwirkende bietet, +- Geheimnisse und privilegierte Zugänge vom Quelltext trennt, +- Fehler und Rollbacks betrieblich beherrschbar macht und +- für einen einzelnen KI-Server ohne unnötig große Managementplattform + betrieben werden kann. + +Die Planung umfasst sowohl die spätere technische Implementierung als auch die +Dokumentation für Administratoren, Entwickler und Deployment-Berechtigte. + +--- + +## 2. Ausgangslage + +Nach dem dokumentierten Stand ist der KI-Server ein Debian-Basissystem unter +`10.0.0.90`. Der geplante Anwendungsstack aus `llama.cpp`, LiteLLM, OpenWebUI +und weiteren Anwendungen ist noch nicht installiert. Gitea ist im bestehenden +Serviceinventar unter `10.0.0.17:3000` geführt; Version, Actions-Konfiguration +und Registry-Funktionen müssen vor der Implementierung live geprüft werden. + +Der bestehende Account `ghost-agent` ist für lesende Prüfungen vorgesehen und +besitzt nach dem letzten Abgleich keine `sudo`-Berechtigung. Er soll nach dem +aktuellen Plan nicht zum privilegierten Deployment-Account erweitert werden. + +Das Repository enthält inzwischen ein minimales Ansible-Control-Projekt mit +Testing-Inventar, einem ausschließlich lesenden Preflight sowie Bootstrap- und +Prüfplaybooks für `ansible-deploy`. Der privilegierte Deployment-Account ist +eingerichtet und verifiziert. Eine erste schreibende Rolle verwaltet die +gemeinsamen Konfigurations- und Datenwurzeln des Hosts; Anwendungsrollen und ein +Production-Inventar existieren noch nicht. Der Preflight selbst nahm keine +Änderungen vor; Bootstrap und Host-Baseline blieben auf die nachfolgend +dokumentierten Zugangs- und Verzeichniszustände begrenzt. + +--- + +## 3. Vorläufiges Zielbild + +Ansible bleibt die einzige technische Quelle für den gewünschten Zustand des +KI-Servers. Gitea verwaltet Quelltext, Reviews, Freigaben und Jobprotokolle. +Getrennte Runner- beziehungsweise Berechtigungsstufen führen CI-Prüfungen, +Testing-Deployments und Production-Deployments aus. Die genaue Anzahl der +Runner wird nach Prüfung der Gitea-Berechtigungsgrenzen festgelegt. + +```mermaid +flowchart LR + DEV[Feature-Branch] --> PR[Pull Request] + PR --> CI[statische CI ohne Serverzugang] + CI --> TESTOK[freigegebener Testing-Lauf] + TESTOK --> TESTRUNNER[Testing-Berechtigung] + TESTRUNNER --> TEST[Testing auf ghostnet-ki] + TEST --> REVIEW[Review und Merge nach main] + REVIEW --> RELEASE[bewusste Production-Freigabe] + RELEASE --> PRODRUNNER[Production-Berechtigung] + PRODRUNNER --> PROD[Production auf ghostnet-ki] + PROD --> VERIFY[Health- und Funktionstests] + + UPSTREAM[versionierte Upstream-Artefakte] --> UPDATE[Versions-Pin im Infrastruktur-Repository] + UPDATE --> PR +``` + +Runner sollen keine eingehenden Verbindungen aus dem Internet benötigen. Sie +holen Jobs bei Gitea ab und benötigen ausgehend nur die für Gitea, Paket- oder +Imagequellen und den jeweiligen SSH-Zugriff zum KI-Server erforderlichen +Verbindungen. CI-Jobs erhalten keinen SSH-Zugriff zum KI-Server. + +--- + +## 4. Ergebnisse der bisherigen Planung + +Die folgende Tabelle unterscheidet bewusst zwischen einer gewählten +Planungsrichtung und noch nicht bestätigten Implementierungsdetails. + +| Thema | Aktueller Planungsstand | Verbindlichkeit / Nachweis | +| --- | --- | --- | +| Konfigurationsmanagement | Ansible beschreibt Basissystem, Laufzeitumgebung, Dienste und Konfiguration | minimales Control-Projekt, lesender Preflight und angewendete erste `host_baseline` vorhanden; Runtime- und Anwendungsrollen fehlen noch | +| Quell- und Freigabeverfahren | Änderungen über Gitea-Branches und Pull Requests; kein regulärer Direkt-Push auf `main` | vorgesehen; Branch-Schutz noch nicht geprüft oder eingerichtet | +| CI | Pull Requests erhalten Lint-, YAML- und Ansible-Syntaxprüfungen ohne produktive Zugangsdaten | vorgesehen; Workflow noch nicht vorhanden | +| Produktionsausführung | Gitea Actions startet Ansible über eine dedizierte Production-Berechtigungsstufe | bevorzugte Lösung; Gitea-Version, Runner-Trennung und Actions-Funktion noch zu prüfen | +| Runner-Platzierung | eigener kleiner LXC, getrennt von Gitea und KI-Server | bevorzugt; Host, Adresse und Ressourcen noch offen | +| Job-Isolation | containerisierte Jobs; für den Gitea Runner ist nach aktueller Herstellerdokumentation Docker der unterstützte Standard | bevorzugt; lokale Freigabe und LXC-Eignung noch offen | +| Parallelität | Produktionsrunner führt höchstens einen Job gleichzeitig aus | vorgesehen; Runner-Konfiguration noch nicht vorhanden | +| Deployment-Freigabe | nach erfolgreichem Review bewusste Freigabe über geschützten `deploy-*`-Tag | bevorzugte Startvariante; Namensschema und berechtigte Teams noch offen | +| Zielzugang | eigener technischer SSH-Account `ansible-deploy` mit separatem, quellnetzbeschränktem Schlüssel und validiertem `NOPASSWD: ALL` | auf dem KI-Server eingerichtet und unabhängig verifiziert; spätere Runner-Übernahme und Rotation offen | +| Geheimnisse | verschlüsselte Ansible-Variablen plus getrennt gespeichertes Vault-Passwort und Runner-Zugangsdaten | Richtung angenommen; konkreter Secret-Workflow noch offen | +| Artefaktversionen | Pakete, Collections und Container-Images werden auf konkrete Versionen, nach Möglichkeit Digests, festgelegt | vorgesehen | +| Repository-Zuschnitt | vorerst ausschließlich das bestehende Repository `infrastructure`; weitere Quellcode-Repositories erst bei tatsächlich eigenständig entwickelten Anwendungen oder MCP-Komponenten | als aktuelle Planungsentscheidung angenommen | +| Anwendungsupdates | Upstream-Artefakte werden im Infrastruktur-Repository auf eine geprüfte Version festgelegt; eigene Build-Repositories sind vorerst nicht Bestandteil des Projekts | vorgesehen; Registry- und Buildverfahren bei späterem Eigenentwicklungsbedarf neu bewerten | +| Automatische Updates | kein unbeaufsichtigtes Produktionsupdate über `latest` oder Watchtower | vorläufige Sicherheitsentscheidung | +| Laufzeitumgebungen | Production und Testing werden als getrennte Stacks auf demselben physischen KI-Server betrieben | als aktuelle Planungsentscheidung angenommen | +| Production | stabiler, extern nutzbarer Stand mit bewusst freigegebenen Versionen | Zielbild; Dienste noch nicht eingerichtet | +| Testing | interner, kurzlebiger oder zurücksetzbarer Stand für kontrollierte Tests vor dem Merge nach `main`; nutzt während eines Wartungsfensters die vollständige Hardware exklusiv | Zielbild; Dienste, Zugriffsweg und Umschaltverfahren noch nicht eingerichtet | +| Ressourcenbetrieb | keine feste physische Aufteilung; Production und Testing laufen nicht gleichzeitig und werden über einen exklusiven Umschaltvorgang zeitlich gestaffelt | als aktuelle Planungsentscheidung angenommen | +| Betriebsnachweis | erfolgreicher Lauf dokumentiert Git-Commit, Zielhost, Zeitpunkt, Prüfungen und Ergebnis | vorgesehen | +| Rollback | Konfigurationsänderung über Revert und erneute Freigabe; persistente Daten zusätzlich über getestete Backups wiederherstellen | Grundprinzip; konkrete SOP noch zu erstellen | + +Eine spätere Umstellung auf ein automatisches Deployment nach jedem Merge in +`main` bleibt möglich. Zu Beginn soll die Freigabe jedoch bewusst und +nachvollziehbar erfolgen, während die technische Ausführung vollständig +automatisiert wird. + +--- + +## 5. Verworfene oder zurückgestellte Varianten + +### 5.1 Ausschließliche Ausführung vom Admin-PC + +Lokale Ansible-Ausführung bleibt für Entwicklung, Diagnose und Notfälle +sinnvoll. Sie soll nicht der reguläre Produktionsweg werden, weil Ausführung, +Werkzeugversionen und Protokolle sonst von einem einzelnen Arbeitsplatz +abhängen. + +### 5.2 `ansible-pull` auf dem KI-Server + +Der Zielserver könnte das Repository periodisch selbst abrufen und lokal +anwenden. Für den einzelnen zentral erreichbaren Server bietet das derzeit +weniger klare Freigaben und eine ungünstigere Trennung zwischen Zielsystem und +Deployment-Steuerung. Die Variante bleibt zurückgestellt. + +### 5.3 AWX + +AWX bietet eine eigene Oberfläche, Rollen, Zeitpläne und Inventarverwaltung. +Der zusätzliche Plattformbetrieb erscheint für den aktuellen Umfang zu groß. +AWX kann erneut bewertet werden, wenn mehrere Zielsysteme, feinere delegierte +Rechte oder umfangreiche Jobpläne verwaltet werden müssen. + +--- + +## 6. Repository-Verantwortung + +Vorerst wird ausschließlich das bestehende Repository `infrastructure` +verwendet. Es soll den vollständigen Sollzustand des KI-Servers, beide +Laufzeitumgebungen und die zugehörigen Workflows besitzen. Damit verändern +nicht mehrere unabhängige Deployment-Repositories denselben Host. + +Vorgesehene grobe Struktur: + +```text +ansible/ +├── ansible.cfg +├── collections/ +│ └── requirements.yml +├── inventories/ +│ ├── production/ +│ └── testing/ +├── playbooks/ +│ ├── host.yml +│ ├── environment.yml +│ └── verify.yml +└── roles/ + ├── host_baseline/ + ├── environment_stack/ + └── weitere_fachrollen/ + +.gitea/workflows/ +├── validate.yml +├── deploy-testing.yml +└── deploy-production.yml +``` + +Diese Struktur ist ein Ausgangspunkt und noch keine Festlegung aller Rollen. +`host.yml` und `environment.yml` sollen gemeinsam den unterstützten +Gesamtzustand herstellen. Ansible-Tags können Diagnose und gezielte Wartung +unterstützen, dürfen aber nicht zu ungetesteten alternativen Installationswegen +führen. + +Unveränderte Drittanbieterkomponenten wie `llama.cpp`, LiteLLM oder OpenWebUI +erhalten derzeit kein eigenes Repository. Ihre freigegebenen Versionen oder +Image-Digests werden im Infrastruktur-Repository geführt. Ein weiteres +Repository wird erst angelegt, wenn tatsächlich eigener, unabhängig +versionierter Quellcode für eine Anwendung, einen MCP Router oder MCP Server +entwickelt wird. Diese spätere Erweiterung ändert nichts daran, dass das +Infrastruktur-Repository allein festlegt, welche Artefaktversion in welcher +Umgebung läuft. + +--- + +## 7. Production und Testing auf einem Host + +### 7.1 Geltungsbereich der Trennung + +Production und Testing laufen zunächst auf demselben physischen KI-Server. +Damit können Anwendungscontainer, Konfigurationen und Daten sauber logisch +getrennt werden. Die Hardware kann wegen des hohen Leistungsbedarfs nicht +dauerhaft zwischen beiden Umgebungen aufgeteilt werden. Stattdessen erhält die +jeweils aktive Umgebung CPU, RAM und beide GPUs exklusiv. Es entstehen dennoch +keine getrennten Fehlerdomänen: + +- Kernel, AMD-Treiber beziehungsweise ROCm, Podman und systemd werden gemeinsam + verwendet; +- ein Hostausfall betrifft beide Umgebungen; +- ein fehlerhafter Testing-Stand kann den gemeinsamen Host beeinträchtigen und + dadurch den Wiederanlauf von Production verzögern; +- Kernel-, Treiber- und Host-Runtime-Updates können nicht realitätsnah in + Testing geprüft werden, ohne den gemeinsamen Host zu verändern. + +Testing ist deshalb für neue Anwendungsstände, Konfigurationen, MCP-Komponenten +und Integrationen vorgesehen. Für risikoarme Vorabtests von Kernel-, Treiber- +oder Podman-Upgrades wäre später ein zweiter Host oder eine ausreichend +vergleichbare separate Maschine erforderlich. + +Die von externen Nutzern erwartete Stabilität von Production gilt außerhalb +angekündigter Test- und Wartungsfenster. Solange nur dieser eine ausgelastete +Host verfügbar ist, verursachen realistische Tests mit voller Hardware eine +geplante Unterbrechung von Production. + +### 7.2 Isolationsziele + +| Bereich | Production | Testing | +| --- | --- | --- | +| Erreichbarkeit | für freigegebene Dienste über NPM; Backend- und Managementports bleiben intern | nur aus freigegebenem LAN/VPN; keine öffentliche NPM-Route | +| Container- und Unit-Namen | eigener eindeutiger Präfix | eigener eindeutiger Präfix | +| Podman-Netz | eigenes internes Netz | eigenes internes Netz ohne Verbindung zum Production-Netz | +| Konfiguration | eigener Verzeichnisbaum | eigener Verzeichnisbaum | +| Persistente Daten | eigene Volumes mit Backup- und Restore-Verfahren | eigene zurücksetzbare Volumes; keine Production-Datenkopie ohne Bereinigung und Freigabe | +| Secrets und API-Keys | ausschließlich Production-Credentials | eigene eingeschränkte Test-Credentials | +| Ports und Hostnamen | stabile freigegebene Endpunkte | getrennte interne Ports und noch festzulegende Testhostnamen | +| Ressourcen | erhält im Normalbetrieb die vollständige Hardware exklusiv | erhält ausschließlich im freigegebenen Testfenster die vollständige Hardware exklusiv | +| GPU | beide GPUs, solange Production aktiv ist | beide GPUs, nachdem Production vollständig beendet wurde | +| Ausführungszustand | standardmäßig aktiv | standardmäßig gestoppt | +| Verfügbarkeit | stabiler Betriebsstand außerhalb angekündigter Wartungsfenster | best effort; wird nach jedem Testlauf wieder vollständig gestoppt | + +Eine gleichzeitige Inferenzlast beider Umgebungen ist nicht vorgesehen. Vor dem +Start von Testing muss nachweisbar sein, dass Production beendet wurde und +keine zugehörigen Prozesse mehr CPU-, RAM- oder GPU-Ressourcen halten. Vor dem +Wiederanlauf von Production muss Testing entsprechend vollständig beendet und +sein temporärer Zustand bereinigt sein. Speicherplatz- und Prozessgrenzen +bleiben auch bei exklusiver Rechenleistung sinnvoll, damit ein Test nicht den +gemeinsamen Datenträger füllt oder den Host dauerhaft beeinträchtigt. + +Große schreibgeschützte Modelldateien können später aus einem gemeinsamen, +nur lesbar eingebundenen Modellspeicher bereitgestellt werden. Datenbanken, +Uploads, Caches mit Schreibzugriff, Benutzerkonten und Anwendungsschlüssel +bleiben vollständig getrennt. Ob ein gemeinsamer Modellspeicher technisch und +betrieblich sinnvoll ist, wird erst mit dem Modell- und Backupkonzept +entschieden. + +### 7.3 Exklusives Umschaltverfahren + +Ein Testing-Deployment ist zugleich ein geplantes Wartungsfenster für +Production. Der vorläufige Ablauf lautet: + +1. konkreten PR-Commit und Testumfang festlegen; +2. Testing-Lauf durch eine berechtigte Person freigeben und Wartungsfenster + dokumentieren; +3. letzten funktionsfähigen Production-Stand und erforderlichen Backupstatus + feststellen; +4. neue externe Anfragen nach Möglichkeit kontrolliert abweisen und Production + geordnet beenden; +5. prüfen, dass alle Production-Units, -Container und GPU-Prozesse beendet + sind; +6. einen hostweiten exklusiven Deployment-Lock setzen; +7. Testing für den festgelegten Commit ausrollen und mit der vollständigen + Hardware starten; +8. Health Checks und fachliche Tests ausführen und Ergebnisse dem Pull Request + zuordnen; +9. Testing unabhängig vom Testergebnis vollständig stoppen; +10. prüfen, dass alle Testing-Units, -Container und GPU-Prozesse beendet sind; +11. Production unverändert auf dem zuletzt freigegebenen Stand starten; +12. Production-Health- und Inferenztests ausführen und erst danach das + Wartungsfenster beenden. + +Der Wiederanlauf von Production darf nicht ausschließlich von einem letzten +Schritt im Gitea-Job abhängen. Runner-Abbruch, Netzwerkverlust oder ein +fehlgeschlagener Test müssen durch einen hostseitigen Recovery-Mechanismus und +eine dokumentierte manuelle SOP beherrscht werden. Die konkrete Umsetzung kann +beispielsweise einen systemd-gesteuerten Umschaltdienst, einen exklusiven Lock, +Timeouts und einen definierten Rückfall auf Production kombinieren; sie wird +erst in der technischen Spezifikation festgelegt. + +Der Testing-Deployment-Account soll Production nicht frei verwalten können. Er +darf lediglich eine eng definierte hostseitige Umschaltoperation starten und +deren Status abfragen. Der Umschaltdienst besitzt die notwendigen Rechte zum +geordneten Stoppen und Wiederanlaufen der beiden Stacks, stellt dem Testing-Job +aber weder Production-Secrets noch allgemeine Production-Verwaltungsrechte zur +Verfügung. + +Production und Testing dürfen niemals gleichzeitig als aktiv markiert sein. +Jeder Start prüft den Gegenstatus und bricht bei einem widersprüchlichen oder +unklaren Zustand ab. + +### 7.4 Vorgesehene Ansible-Abbildung + +Gemeinsame Hostaufgaben und umgebungsspezifische Anwendungsaufgaben werden +getrennt: + +```text +ansible/ +├── inventories/ +│ ├── production/ +│ │ ├── hosts.yml +│ │ ├── group_vars/ +│ │ └── host_vars/ +│ └── testing/ +│ ├── hosts.yml +│ ├── group_vars/ +│ └── host_vars/ +├── playbooks/ +│ ├── host.yml +│ ├── environment.yml +│ └── verify.yml +└── roles/ + ├── host_baseline/ + └── environment_stack/ +``` + +Beide Inventare zeigen auf `ghostnet-ki`, enthalten aber ausschließlich die +für ihre Umgebung zulässigen Variablen und Secrets. `host.yml` verwaltet +gemeinsame Pakete, Treiber, Benutzer und Runtime-Komponenten einmalig. +`environment.yml` muss genau eine explizit gewählte Umgebung verwalten und vor +Änderungen prüfen, ob Namen, Pfade, Ports und Zielvariablen zur Auswahl passen. +Hoständerungen dürfen nicht beiläufig durch einen Testing-Lauf erfolgen. + +Für den ersten umgesetzten Baseline-Stand gelten getrennte Konfigurationsbäume +unter `/etc/ghostnet-ai/{production,testing}` und getrennte persistente +Datenbäume unter `/var/lib/ghostnet-ai/{production,testing}`. Der Pfad +`/var/lib/ghostnet-ai/shared` ist für später ausdrücklich freigegebene +gemeinsame Daten reserviert. Konkrete Unterverzeichnisse, Eigentümer sowie +Unit- und Container-Namen werden mit den jeweiligen Fachrollen festgelegt. + +--- + +## 8. Vorgesehener Workflow + +### 8.1 Änderung und Prüfung + +1. Repository klonen und dokumentierte lokale Ansible-Umgebung einrichten. +2. Feature-Branch anlegen. +3. Rollen, Variablen, Templates und zugehörige Dokumentation ändern. +4. lokale Lint-, Syntax- und nach Möglichkeit Check-Mode-Prüfungen ausführen. +5. Pull Request erstellen. +6. automatisierte CI-Prüfungen ohne produktive SSH- oder Vault-Zugangsdaten + ausführen. +7. erste menschliche Prüfung und einen Testing-Lauf für den konkreten Commit + freigeben; +8. Testing-Ergebnis im Pull Request dokumentieren; +9. fachliches Abschlussreview und Freigabe durch eine zweite berechtigte + Person und +10. nach erfolgreichem Statuscheck in den geschützten `main`-Branch mergen. + +### 8.2 Testing-Deployment vor dem Merge + +Ein Pull Request führt zunächst nur statische CI-Prüfungen in einer isolierten +Jobumgebung ohne SSH-, Vault- oder Serverzugang aus. Nach einer ersten +menschlichen Prüfung kann eine berechtigte Person einen kontrollierten +Testing-Lauf für den konkreten Commit auslösen. + +Der Testing-Lauf: + +1. verwendet ausschließlich den Testing-Deployment-Account und + Testing-Secrets; +2. erhält erst nach einem freigegebenen Wartungsfenster Zugriff auf den + Umschaltvorgang; +3. fordert über die fest definierte hostseitige Umschaltoperation den + kontrollierten Production-Stopp an und prüft den vollständigen Stillstand; +4. darf im aktiven Testabschnitt nur Testing-Pfade, -Units, -Container und + -Netze verwalten; +5. zeichnet den getesteten Commit unverwechselbar auf; +6. führt technische Health Checks und die für die Änderung notwendigen + Funktionstests aus; +7. veröffentlicht keine Testdienste im Internet; +8. kann die Testing-Daten vor oder nach dem Lauf auf einen bekannten Stand + zurücksetzen und +9. muss Testing stoppen sowie den letzten freigegebenen Production-Stand + wieder starten und prüfen, auch wenn der eigentliche Test fehlschlägt. + +Beliebiger Pull-Request-Code darf nicht automatisch mit privilegierten +Serverzugängen ausgeführt werden. Runner-Labels allein gelten nicht als +ausreichende Berechtigungsgrenze. Vor der Implementierung ist zu prüfen, wie +Gitea in der eingesetzten Version Secrets, interne Branches, Forks und manuell +freigegebene Jobs behandelt. Production-Credentials werden keinem +Pull-Request-Workflow bereitgestellt. + +### 8.3 Production-Deployment + +1. berechtigte Person erzeugt einen geschützten Deployment-Tag auf einem + geprüften Commit; +2. der repository-spezifische Runner übernimmt den Job mit einer Parallelität + von eins; +3. der Job prüft erneut Syntax und Abhängigkeiten; +4. optionaler Check-Modus zeigt erwartete Änderungen, soweit die verwendeten + Module ihn zuverlässig unterstützen; +5. `environment.yml` wendet den Production-Sollzustand auf `ghostnet-ki` an; +6. `verify.yml` prüft Dienste, interne Endpunkte, Containerzustand und später + einen kleinen Inferenztest; +7. der Lauf hält Commit, Imageversionen oder Digests sowie Prüfergebnis fest; +8. erst nach allen Prüfungen gilt das Deployment als erfolgreich. + +Ein Production-Deployment verwendet ausschließlich einen Commit aus dem +geschützten `main`-Branch und eine eigene bewusste Freigabe. Ein erfolgreicher +Testing-Lauf ist Voraussetzung für fachlich relevante Änderungen, ersetzt aber +weder Review noch Production-Backup und Freigabe. + +Check Mode ist nur eine zusätzliche Kontrolle. Module und Shell-Kommandos, die +keine vollständige Simulation unterstützen, müssen durch gezielte Tests und +idempotente Implementierung abgesichert werden. + +--- + +## 9. Zugangsdaten und Sicherheitsgrenzen + +Der Produktionsrunner verarbeitet privilegierte Zugangsdaten und muss daher +wie ein Administrationssystem behandelt werden. Vorgesehen sind: + +- repository-spezifische Runner-Registrierung und eindeutiges Runner-Label, +- keine Ausführung fremder oder öffentlicher Repositories, +- ein technischer Zielaccount getrennt von persönlichen Konten und + `ghost-agent`, +- getrennte technische Berechtigungen und Schlüssel für Production und + Testing, soweit dies mit der gewählten Podman-/systemd-Verwaltung belastbar + durchsetzbar ist, +- ein eigener SSH-Schlüssel ausschließlich für das Deployment, +- geprüfter und fest hinterlegter SSH-Host-Key, +- keine Passwort- oder Root-Anmeldung per SSH, +- möglichst eng begrenzter Netzwerkzugang des Runner-LXC, +- keine Secrets in Repository, Jobausgaben oder diff-fähigen Templates, +- `no_log` und deaktivierte Diffs für geheimnishaltige Tasks, +- getrennte Aufbewahrung des Vault-Passworts im Runner und im administrativen + Passwortmanager sowie +- dokumentierte Rotation und Sperrung von Runner-, SSH- und Vault-Zugangsdaten. + +Der Testing-Zugang darf keinen Zugriff auf Production-Secrets oder +Production-Daten erhalten. Die Produktionsberechtigung darf nicht allein durch +die Auswahl eines frei benennbaren Runner-Labels erreichbar sein. Das genaue +Verfahren wird nach Prüfung der eingesetzten Gitea-Version festgelegt. Die +notwendige zeitliche Umschaltung wird über eine eng definierte hostseitige +Operation vermittelt und ist keine allgemeine Production-Berechtigung des +Testing-Accounts. + +Der Deployment-Account besitzt derzeit bewusst `NOPASSWD: ALL`, weil +Paketverwaltung, systemd, Treiber und rootful Containerverwaltung weitreichende +Rechte erfordern. Eine formal kleine Befehlsliste wäre nur sinnvoll, wenn sie +den tatsächlichen Ansible-Betrieb nicht durch unsichere Umgehungen oder fragile +Sonderfälle ersetzt. Schlüssel, Account und berechtigte Workflows sind deshalb +als root-äquivalente Sicherheitsgrenze zu behandeln. Die spätere Trennung von +Production- und Testing-Berechtigungen bleibt davon unabhängig zu spezifizieren. + +--- + +## 10. Zu erstellende technische Dokumentation und SOPs + +Die folgenden Dokumente sind vorgesehen. Titel, Dokumenten-IDs und Zuschnitt +werden bei der Ausarbeitung final festgelegt. + +| Arbeitsname | Inhalt | Zielgruppe | +| --- | --- | --- | +| Technische Spezifikation der Ansible-Plattform | Komponenten, Vertrauensgrenzen, Repository-Struktur, Inventar, Rollenmodell, Runner, Netzpfade, Production-/Testing-Isolation, Secret-Konzept, Versionierung und Abnahmekriterien | Infrastrukturadministratoren und Reviewer | +| SOP: Gitea Runner einrichten | LXC-Voraussetzungen, Installation, Registrierung, Labels, Job-Isolation, Netzwerkregeln, Logging, Updates, Backup und Funktionsabnahme | Server Admin | +| SOP: KI-Server für Ansible vorbereiten | Deployment-Account, SSH-Key, Host-Key, `sudo`, Python-Voraussetzungen, Bootstrap und negative Zugriffstests | Server Admin | +| SOP: Lokales Ansible-Environment einrichten | unterstütztes Betriebssystem, Repository-Checkout, Python-/Ansible-Versionen, Collections, Linting, Vault-Zugriff und lokale Prüfkommandos | Mitwirkende und Reviewer | +| SOP: Änderung und Pull Request | Branching, Rollen- und Variablenänderungen, Dokumentationspflicht, lokale Tests, Review und Merge | Mitwirkende | +| SOP: Deployment ausführen und prüfen | Freigabe-Tag, Actions-Lauf, erwartete Prüfungen, Auswertung, Betriebsnachweis und Fehlerbehandlung | Deployment-Berechtigte | +| SOP: Testing-Umgebung verwenden | Teststand und Wartungsfenster auswählen, kontrollierten Lauf freigeben, Production geordnet umschalten, interne Endpunkte verwenden, Testdaten zurücksetzen, Production wiederherstellen und Ergebnisse im Pull Request dokumentieren | Mitwirkende und Reviewer | +| SOP: Rollback und Wiederherstellung | Revert, erneutes Deployment, Image-Rückkehr, Backup/Restore, Datenmigrationen und Abnahmetests | Server Admin | +| SOP: Geheimnisse verwalten | Vault-Dateien, Passwortaufbewahrung, Runner-Secrets, Rotation, Widerruf und Verhalten bei möglicher Offenlegung | Server Admin und Security-Verantwortliche | + +Die Nutzer-SOPs sollen konkrete Voraussetzungen, Befehle, erwartete Ergebnisse, +Fehlerfälle und Abnahmekriterien enthalten. Eine Zielbeschreibung allein gilt +nicht als Nachweis einer erfolgreichen Einrichtung. + +--- + +## 11. Vorgesehene Umsetzungsphasen + +### Phase A – Ist-Aufnahme und Festlegung + +- Gitea-Version, Actions, Runner-Kompatibilität und Registry prüfen; +- vorbereitete Repositories und Zuständigkeiten aufnehmen; +- Runner-LXC, Netzwerkpfad und Ressourcen festlegen; +- Isolation, exklusiven Umschaltmechanismus, Wartungsfenster und interne + Erreichbarkeit von Production und Testing festlegen; +- eingerichteten Deployment-Account in das endgültige Secret-, Rotations- und + Runner-Verfahren überführen; +- Backup- und Restore-Voraussetzungen klären. + +### Phase B – Ansible-Grundgerüst + +- Repository-Struktur, Inventar und `ansible.cfg` anlegen; +- Werkzeug- und Collection-Versionen festlegen; +- Basisrolle und Verifikationsplaybook entwickeln; +- getrennte Inventare und Variablenvalidierung für Production und Testing + erstellen; +- lokale Entwickler-SOP erstellen und gegen einen frischen Checkout testen. + +### Phase C – Gitea-Integration + +- Runner-LXC reproduzierbar einrichten und abnehmen; +- Validierungsworkflow ohne Produktionszugang einführen; +- Branch- und Tag-Schutz konfigurieren; +- Deployment-Workflow mit einem ungefährlichen Testplaybook abnehmen; +- Testing-Deployment vor dem Merge und Production-Freigabe nach dem Merge + getrennt abnehmen. + +### Phase D – KI-Laufzeit + +- Podman und systemd-Integration; +- AMD-GPU-/ROCm- oder alternative Laufzeit nach technischer Prüfung; +- `llama.cpp` und lokaler Funktionstest; +- Wiederanlauf nach geplantem Reboot prüfen. + +### Phase E – Anwendungen und Veröffentlichung + +- LiteLLM, OpenWebUI und weitere beschlossene Anwendungen; +- persistente Daten, Backups und Restore-Test; +- interne Dienste und NPM-Veröffentlichung getrennt abnehmen; +- Authentifizierung, API-Keys, Health Monitoring und Betriebs-SOPs ergänzen. + +--- + +## 12. Offene Entscheidungen und Nachweise + +Vor der technischen Spezifikation sind mindestens folgende Punkte zu klären: + +1. Welche Gitea-Version läuft, und sind Actions sowie geschützte Tags in der + eingesetzten Version aktiviert und geeignet? +2. Welche Komponenten werden zunächst ausschließlich als versionierte + Upstream-Artefakte eingebunden, und für welche ist mittelfristig eigener + Quellcode zu erwarten? +3. Wo wird der Runner-LXC betrieben, welche Adresse erhält er, und darf dort + Docker für isolierte Jobs eingesetzt werden? +4. Soll die erste Produktionsfreigabe über einen geschützten Tag oder zunächst + über einen manuell gestarteten Workflow erfolgen? +5. Welche Personen oder Gitea-Teams dürfen reviewen, mergen und Deployments + freigeben? +6. Wie wird der bestehende root-äquivalente Schlüssel von der lokalen + Control-Umgebung auf den späteren Runner überführt beziehungsweise rotiert, + und wie werden Production- und Testing-Berechtigungen dabei getrennt? +7. Wird Ansible Vault verwendet, oder soll ein anderer vorhandener + Secret-Store angebunden werden? +8. Werden eigene Images in der Gitea Registry gebaut, und wie werden + Registry-Credentials und Image-Signaturen behandelt? +9. Welche Backupziele, Aufbewahrungsfristen und nachgewiesenen Restore-Verfahren + gelten für Konfiguration, Modelle und Anwendungsdaten? +10. Welche Versionen und Installationswege werden für AMD-GPU-Laufzeit, + `llama.cpp`, LiteLLM, OpenWebUI und weitere Anwendungen unterstützt? +11. Welche ausgehenden Internetzugriffe erhalten Runner und KI-Server für + Pakete, Collections, Images und Modelle? +12. Welche Health Checks und fachlichen Inferenztests bilden die verbindliche + Abnahme eines Deployments? +13. Welche maximale Dauer, Häufigkeit und Ankündigungsfrist gelten für + Testing-Wartungsfenster, in denen Production nicht verfügbar ist? +14. Welcher hostseitige Lock-, Timeout- und Recovery-Mechanismus stellt den + Wiederanlauf von Production sicher, wenn Runner oder Testing ausfallen? +15. Welche internen Hostnamen und Ports erhält Testing, und welche LAN-/VPN- + Rollen dürfen darauf zugreifen? +16. Können Production und Testing mit getrennten unprivilegierten Accounts + verwaltet werden, oder erfordert die gewählte Podman-/systemd-Architektur + einen stärker privilegierten gemeinsamen Deploymentpfad? +17. Wie wird ein Testing-Deployment eines Pull-Request-Commits in der + eingesetzten Gitea-Version autorisiert, ohne Production-Secrets oder + Production-Runner für veränderbaren PR-Code verfügbar zu machen? +18. Welche Testdaten dürfen verwendet werden, und wie wird ausgeschlossen, + dass personenbezogene oder geheime Production-Daten nach Testing kopiert + werden? + +Diese Fragen werden nicht durch Annahmen in Playbooks ersetzt. Entscheidungen +werden zunächst in der technischen Spezifikation dokumentiert und anschließend +in Implementierung und SOPs übernommen. + +--- + +## 13. Referenzstand der Planung + +Für die weitere Ausarbeitung wurden am 19.09.2026 insbesondere folgende +Herstellerdokumentationen herangezogen: + +- [Gitea Actions – Überblick](https://docs.gitea.com/usage/actions/) +- [Gitea Runner](https://docs.gitea.com/runner/) +- [Gitea Actions – Secrets](https://docs.gitea.com/usage/actions/secrets/) +- [Gitea Actions – Job-Token-Berechtigungen](https://docs.gitea.com/usage/actions/token-permissions/) +- [Gitea – geschützte Tags](https://docs.gitea.com/usage/protected-tags/) +- [Ansible Playbooks](https://docs.ansible.com/projects/ansible/latest/playbook_guide/playbooks_intro.html) +- [Ansible Check- und Diff-Modus](https://docs.ansible.com/projects/ansible/latest/playbook_guide/playbooks_checkmode.html) +- [Ansible Vault](https://docs.ansible.com/projects/ansible/latest/vault_guide/vault.html) + +Vor der Implementierung sind versionsspezifische Aussagen gegen die dann +tatsächlich eingesetzten Gitea-, Runner- und Ansible-Versionen zu prüfen. + +--- + +## 14. Belegter Ansible-Implementierungsstand + +Am 19.09.2026 wurde `ansible-core` 2.21.4 in einer lokalen, von Git +ausgeschlossenen virtuellen Umgebung installiert. Das neue Control-Projekt +enthält: + +- `ansible.cfg` mit festem Testing-Inventar, Host-Key-Prüfung und lokalem + Ansible-Arbeitsverzeichnis; +- ein Testing-Inventar für `ghostnet-ki` unter `10.0.0.90` mit dem + unprivilegierten Konto `ghost-agent`; +- ein Preflight-Playbook ohne `become` und ohne konfigurationsändernde Tasks; +- eine kurze Anleitung für Control-Node-Setup, SSH-Agent, Syntaxprüfung und + Live-Lauf. + +Inventardarstellung und Syntaxprüfung waren erfolgreich. Der Live-Lauf +bestätigte: + +| Merkmal | Ergebnis | +| --- | --- | +| Hostname | `ghostnet-ki` | +| Betriebssystem | Debian 13.6 | +| Standard-IPv4 | `10.0.0.90` | +| CPU | 14 vCPUs | +| RAM | 64.123 MiB | +| Remote-Python | 3.13.5 | +| GPUs | zwei Navi-21-PCI-Geräte `1002:73bf` | +| Play Recap | `ok=5 changed=0 unreachable=0 failed=0` | + +Dieser Lauf ist ein Zugriffs- und Identitätsnachweis. Er bestätigt weder ROCm +noch Inferenzfunktion, privilegierte Deploymentrechte, Containerbetrieb oder +die geplante Umschaltung zwischen Production und Testing. + +Für den folgenden Bootstrap wurden ein eigener Ed25519-Schlüssel, ein +Account-/sudoers-Playbook und ein getrenntes Prüfplaybook erstellt. Der +öffentliche Schlüssel ist mit Fingerprint +`SHA256:IKEInbIo8VVI1USg5+a3ypGK8H8FweJiQu+v7KvhQDU` versioniert; der private +Schlüssel liegt mit Modus `0600` außerhalb des Repositories. Syntaxprüfung und +Bootstrap-Inventar waren erfolgreich. + +Der Bootstrap wurde anschließend über das persönliche Administratorkonto +ausgeführt. Benutzer, Home-/SSH-Verzeichnisse, eingeschränkter Public Key und +die mit `visudo` geprüfte sudoers-Datei wurden erfolgreich angelegt. Die letzte +Assertion des ersten Laufs schlug ausschließlich fehl, weil die nicht vollständig +quotierte Bedingung mit `NOPASSWD: ALL` als YAML-Mapping statt als String +interpretiert wurde. Die Bedingung wurde korrigiert. Das getrennte Prüfplaybook +bestätigte danach die Anmeldung als `ansible-deploy`, normale Modulausführung +und nicht-interaktive Eskalation auf UID 0 mit +`ok=5 changed=0 unreachable=0 failed=0`. Ein vollständiger zweiter +Bootstrap-Lauf zur praktischen Idempotenzprüfung wurde noch nicht ausgeführt. + +Als erste gemeinsame Hostrolle wurde anschließend `host_baseline` mit eigenem +Host-Inventar und `playbooks/host.yml` implementiert. Vor Änderungen prüft sie +Hostname, Debian-Hauptversion, Standard-IPv4, die Erkennung des +Root-Dateisystems und mindestens 100 GiB freien Speicher. Sie verwaltet derzeit +ausschließlich folgende root-eigenen Verzeichniswurzeln: + +```text +/etc/ghostnet-ai/production +/etc/ghostnet-ai/testing +/var/lib/ghostnet-ai/production +/var/lib/ghostnet-ai/testing +/var/lib/ghostnet-ai/shared +``` + +Inventar- und Syntaxprüfung waren erfolgreich. Der Check-Modus zeigte nur die +sieben erwarteten Verzeichnisse einschließlich ihrer Eltern als Änderungen und +meldete 1.677,7 GiB freien Platz auf `/`. Der erste reale Lauf endete mit +`ok=6 changed=1 unreachable=0 failed=0`. Der unmittelbar folgende identische +Lauf endete mit `ok=6 changed=0 unreachable=0 failed=0` und belegt die +Idempotenz des aktuellen Rollenumfangs. Es wurden keine Pakete installiert und +keine Dienste, Container oder GPU-Komponenten verändert. + +--- + +## 15. Abschlussgrenze dieses Entwurfs + +Mit Version 0.7 sind der aktuelle Planungsstand, der ausdrücklich begrenzte +Ansible-Preflight, der verifizierte Deployment-Account und die angewendete +minimale Host-Baseline persistiert. +Insbesondere wurden noch nicht: + +- ein Gitea Runner angelegt, +- Gitea Actions oder Schutzregeln konfiguriert, +- Runtime- oder Anwendungsrollen, ein Production-Inventar oder ein Vault erstellt, +- eine Production- oder Testing-Umgebung eingerichtet oder voneinander + abgenommen, +- Pakete oder Anwendungen auf dem KI-Server installiert, +- Backups oder Rollbacks praktisch getestet oder +- die geplante Deploymentkette technisch abgenommen. + +Der nächste Dokumentationsschritt ist die Ist-Aufnahme aus Phase A. Auf deren +Ergebnissen baut die verbindliche technische Spezifikation auf.