GNAI-007: Basisfunktionen für Ansible erstellt.

This commit is contained in:
2026-09-19 23:20:35 +02:00
parent 7d1a3228ef
commit de06eb6129
18 changed files with 1227 additions and 2 deletions
+5
View File
@@ -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
+4 -2
View File
@@ -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.
+140
View File
@@ -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.
+13
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJE3UGGHyB+CMA2sqPpI/6efn9u3eLelL2MkHqdt2naB ansible-deploy@ghostnet-control-2026-09
+8
View File
@@ -0,0 +1,8 @@
---
all:
children:
llm_servers:
hosts:
ghostnet-ki:
ansible_host: 10.0.0.90
ansible_user: ghost
+8
View File
@@ -0,0 +1,8 @@
---
all:
children:
llm_servers:
hosts:
ghostnet-ki:
ansible_host: 10.0.0.90
ansible_user: ansible-deploy
+8
View File
@@ -0,0 +1,8 @@
---
all:
children:
llm_servers:
hosts:
ghostnet-ki:
ansible_host: 10.0.0.90
ansible_user: ghost-agent
@@ -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.
+12
View File
@@ -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
+60
View File
@@ -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 }}"
@@ -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.
+2
View File
@@ -0,0 +1,2 @@
# ansible-core 2.21 supports Python 3.12 through 3.14 on the control node.
ansible-core==2.21.4
+17
View File
@@ -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.
@@ -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"
@@ -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 }}"
+16
View File
@@ -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.
+711
View File
@@ -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.