--- # guacd + FreeRDP als reine Protokoll-Engine fuer RDP (Konzept 3/4.3). # Laeuft ausschliesslich lokal gebunden, kein Netzwerkzugriff von aussen. # # Installationsweg haengt vom Ziel-OS ab: # # - Ubuntu (22.04/24.04): hat ein fertiges apt-Paket ("guacd", # "libguac-client-rdp0", ueber die "universe"-Komponente). Zwar mit # veralteter Version (1.3.0), aber offiziell von Ubuntu gepflegt und # bekommt automatisch Sicherheitsupdates ueber unattended-upgrades (da # dynamisch gegen libfreerdp2 gelinkt). Daher hier bevorzugt. # # - Debian: hat KEIN guacd-apt-Paket (weder bookworm noch trixie -- nur # Ubuntu paketiert es ueber "universe"). Daher wird guacd hier gemaess der # offiziellen Apache-Guacamole-Doku ("Installing Guacamole natively") aus # dem Quellcode gebaut. Nur das RDP-Plugin wird aktiviert # (FreeRDP-Dev-Header vorhanden, kein VNC/Telnet), passend zum reinen # RDP-Einsatzzweck dieses Jumphosts. # # FreeRDP-Paketname und Build-Quelle haengen dabei von der Debian-Version ab: # # - Debian 12 (bookworm): liefert nur FreeRDP2 (freerdp2-dev). Dafuer wird # der offizielle guacamole-server-1.5.5-Release-Tarball gebaut (stabil, # Checksum-verifiziert). # # - Debian 13 (trixie): liefert nur FreeRDP3 (freerdp3-dev), und zwar # bereits Version 3.15 -- NEUER als das, wogegen der letzte offizielle # guacamole-server-Release (1.6.0) geschrieben wurde. Sowohl der # 1.6.0-Tarball als auch der aktuelle "main"-Branch (git, Stand: Aug 2026) # bauen NICHT gegen trixies FreeRDP3 (u.a. entfernter "->input"-Member # auf rdpFreerdp, s. https://issues.apache.org/jira/browse/GUACAMOLE-2146 # und diverse Debian-13-Install-Guides, die genau das bestaetigen). # Es gibt noch KEIN Release oder git-main-Stand mit funktionierender # FreeRDP-3.15-Unterstuetzung. Der git-main-Pfad bleibt hier dennoch als # Option bestehen (fuer den Fall, dass ein zukuenftiger Commit das # behebt), ist aber AUSDRUECKLICH KEIN von Apache Guacamole empfohlener # Weg (Doku: "please not a non-release version from git") und aktuell # NICHT funktionsfaehig getestet. Fuer produktive Debian-13-Hosts also # bis auf Weiteres eher Ubuntu oder Debian 12 als Zielsystem waehlen. # Sobald ein Fix verfuegbar ist: guacd_git_ref auf den entsprechenden # Commit/Branch setzen, oder bei einem echten Release guacd_version auf # den Tag setzen (erzwingt den stabilen Release-Tarball-Pfad). - name: Installationsweg (apt vs. Quellcode) und ggf. FreeRDP-Paket/Version anhand der Distribution bestimmen ansible.builtin.set_fact: _guacd_install_mode: "{{ 'apt' if ansible_distribution == 'Ubuntu' else 'source' }}" _guacd_build_needed: false _guacd_src_dir: "" _guacd_freerdp_pkg: >- {{ guacd_freerdp_dev_package if (guacd_freerdp_dev_package | default('', true) | length > 0) else ('freerdp3-dev' if (ansible_distribution == 'Debian' and ansible_distribution_major_version | int >= 13) else 'freerdp2-dev') }} _guacd_build_mode: >- {{ 'release' if (guacd_version | default('', true) | length > 0) else ('git' if (ansible_distribution == 'Debian' and ansible_distribution_major_version | int >= 13) else 'release') }} _guacd_version: "{{ guacd_version if (guacd_version | default('', true) | length > 0) else '1.5.5' }}" _guacd_git_ref: "{{ guacd_git_ref | default('main') }}" # --- apt-Pfad (Ubuntu) ------------------------------------------------------ - name: 'guacd und FreeRDP-Plugin per apt installieren (Ubuntu: guacd liegt in "universe")' ansible.builtin.apt: name: - guacd - libguac-client-rdp0 state: present update_cache: true when: _guacd_install_mode == 'apt' # --- Quellcode-Pfad (Debian) ------------------------------------------------- - name: Build-Abhaengigkeiten fuer guacd installieren ansible.builtin.apt: name: - build-essential - libcairo2-dev - libjpeg62-turbo-dev - libpng-dev - libtool-bin - uuid-dev - libpango1.0-dev - libssl-dev - "{{ _guacd_freerdp_pkg }}" - pkg-config - wget - ca-certificates - git - autoconf - automake - libtool state: present update_cache: true when: _guacd_install_mode == 'source' - name: Vorhandene guacd-Quellcode-Installation pruefen ansible.builtin.stat: path: /usr/local/sbin/guacd register: _guacd_bin when: _guacd_install_mode == 'source' - name: Installierte guacd-Version ermitteln (Release-Pfad) ansible.builtin.command: /usr/local/sbin/guacd -v register: _guacd_installed_version changed_when: false when: _guacd_install_mode == 'source' and _guacd_bin.stat.exists and _guacd_build_mode == 'release' - name: guacamole-server Quellcode-Tarball herunterladen ansible.builtin.get_url: url: "https://archive.apache.org/dist/guacamole/{{ _guacd_version }}/source/guacamole-server-{{ _guacd_version }}.tar.gz" dest: "/usr/local/src/guacamole-server-{{ _guacd_version }}.tar.gz" checksum: "sha256:https://archive.apache.org/dist/guacamole/{{ _guacd_version }}/source/guacamole-server-{{ _guacd_version }}.tar.gz.sha256" mode: "0644" when: >- _guacd_install_mode == 'source' and _guacd_build_mode == 'release' and ((not _guacd_bin.stat.exists) or (_guacd_version not in (_guacd_installed_version.stdout | default('')))) - name: guacamole-server Tarball entpacken ansible.builtin.unarchive: src: "/usr/local/src/guacamole-server-{{ _guacd_version }}.tar.gz" dest: /usr/local/src remote_src: true creates: "/usr/local/src/guacamole-server-{{ _guacd_version }}/configure" when: >- _guacd_install_mode == 'source' and _guacd_build_mode == 'release' and ((not _guacd_bin.stat.exists) or (_guacd_version not in (_guacd_installed_version.stdout | default('')))) - name: Feststellen, ob (Neu-)Build noetig ist (Release-Pfad) ansible.builtin.set_fact: _guacd_build_needed: "{{ (not _guacd_bin.stat.exists) or (_guacd_version not in (_guacd_installed_version.stdout | default(''))) }}" _guacd_src_dir: "/usr/local/src/guacamole-server-{{ _guacd_version }}" when: _guacd_install_mode == 'source' and _guacd_build_mode == 'release' - name: guacamole-server-Quellcode (git {{ _guacd_git_ref }}) holen/aktualisieren ansible.builtin.git: repo: https://github.com/apache/guacamole-server.git dest: /usr/local/src/guacamole-server-git version: "{{ _guacd_git_ref }}" force: true register: _guacd_git when: _guacd_install_mode == 'source' and _guacd_build_mode == 'git' - name: Feststellen, ob (Neu-)Build noetig ist (git-Pfad) ansible.builtin.set_fact: _guacd_build_needed: "{{ (not _guacd_bin.stat.exists) or _guacd_git.changed }}" _guacd_src_dir: /usr/local/src/guacamole-server-git when: _guacd_install_mode == 'source' and _guacd_build_mode == 'git' - name: Build-System aus configure.ac generieren (autoreconf, nur git-Pfad) ansible.builtin.command: cmd: autoreconf -fi chdir: "{{ _guacd_src_dir }}" when: _guacd_install_mode == 'source' and _guacd_build_mode == 'git' and _guacd_build_needed - name: guacamole-server konfigurieren (nur RDP-Plugin, kein VNC/Telnet/guacenc) # --sysconfdir=/etc ist Pflicht (Befund 3, Umsetzungsauftrag_Sonnet5.md # Teil A.2): ohne diesen Schalter sucht guacd sein Konfigverzeichnis unter # /usr/local/etc/guacamole, waehrend die Tasks weiter unten unveraendert # nach /etc/guacamole schreiben. Solange guacd_port/bind_host auf den # (unwirksamen) Defaults stehen faellt das nicht auf; sobald einer davon # abweicht, lauscht guacd auf dem falschen Port/Host und die App bekommt # "[Errno 111] Connection refused". ansible.builtin.command: cmd: >- ./configure --with-systemd-dir=/etc/systemd/system --sysconfdir=/etc --disable-guacenc --disable-guaclog chdir: "{{ _guacd_src_dir }}" when: _guacd_install_mode == 'source' and _guacd_build_needed - name: guacamole-server kompilieren ansible.builtin.command: cmd: "make -j{{ ansible_processor_vcpus | default(2) }}" chdir: "{{ _guacd_src_dir }}" when: _guacd_install_mode == 'source' and _guacd_build_needed changed_when: true - name: guacamole-server installieren (make install) ansible.builtin.command: cmd: make install chdir: "{{ _guacd_src_dir }}" when: _guacd_install_mode == 'source' and _guacd_build_needed notify: - reload systemd - restart guacd - name: Shared-Library-Cache aktualisieren (ldconfig) ansible.builtin.command: ldconfig when: _guacd_install_mode == 'source' and _guacd_build_needed changed_when: false - name: Pruefen, dass das RDP-Plugin tatsaechlich gebaut und installiert wurde # Der Build ueberspringt das RDP-Plugin STILLSCHWEIGEND, wenn die # FreeRDP-Header (_guacd_freerdp_pkg) beim configure-Lauf fehlten oder # nicht gefunden wurden -- "make"/"make install" laufen trotzdem # fehlerfrei durch. Ohne diese Pruefung faellt das erst zur Laufzeit auf, # als "Support for protocol "rdp" is not installed" von guacd, mitten in # einem Verbindungsversuch (Umsetzungsauftrag_Sonnet5.md Teil A.2, "Zu # tun"-Liste). Beide moeglichen Installationspfade des Plugins abdecken: # /usr/local/lib direkt (aeltere guacamole-server-Versionen) und # /usr/local/lib/guacamole (aktuelle Versionen, eigenes Plugin-Verzeichnis). ansible.builtin.find: paths: - /usr/local/lib - /usr/local/lib/guacamole patterns: "libguac-client-rdp.so*" recurse: false register: _guacd_rdp_plugin when: _guacd_install_mode == 'source' - name: Abbrechen, wenn das RDP-Plugin fehlt ansible.builtin.fail: msg: >- guacd wurde gebaut, aber libguac-client-rdp.so* wurde weder unter /usr/local/lib noch /usr/local/lib/guacamole gefunden. Vermutliche Ursache: die FreeRDP-Entwicklungspakete ({{ _guacd_freerdp_pkg }}) waren beim './configure'-Lauf nicht vollstaendig installiert, das RDP-Plugin wurde daraufhin stillschweigend uebersprungen. Pruefen: 'apt policy {{ _guacd_freerdp_pkg }}' auf dem Zielsystem, danach den Build-Ordner ({{ _guacd_src_dir }}) loeschen und die Rolle erneut ausfuehren, damit configure/make sauber neu laufen. when: _guacd_install_mode == 'source' and (_guacd_rdp_plugin.matched | default(0)) == 0 # --- Gemeinsame Konfiguration (beide Pfade) --------------------------------- - name: Dedizierten guacd-User sicherstellen (apt-Paket legt ihn i.d.R. bereits an) # Befund B6: als Zweitgruppe "{{ jumphost_app_group }}" (append, nicht # ersetzen) bekommt guacd das Traversal-Bit auf {{ jumphost_data_dir }} # (siehe Rolle sqlite_init) und kann damit ueberhaupt erst bis zu # rdp-drives/ vordringen. Ohne diese Zeile schlaegt jede RDP-Sitzung mit # aktivierter Laufwerksumleitung mit EACCES fehl. ansible.builtin.user: name: guacd system: true shell: /usr/sbin/nologin create_home: false groups: "{{ jumphost_app_group }}" append: true notify: restart guacd # neue Gruppenmitgliedschaft wirkt erst nach einem Neustart des Prozesses - name: Konfigurationsverzeichnis /etc/guacamole anlegen # Befund 4 (Umsetzungsauftrag_Sonnet5.md Teil A.2): lineinfile mit # create:true legt nur die FEHLENDE DATEI an, nicht das Verzeichnis -- # ohne diesen Task bricht die Rolle bei den beiden folgenden lineinfile- # Tasks ab, wenn /etc/guacamole noch nicht existiert (frischer Host, oder # apt-Pfad ohne vorherigen Paketaufruf). ansible.builtin.file: path: /etc/guacamole state: directory owner: root group: root mode: "0755" - name: guacd.conf vollstaendig ausrollen # Ersetzt vier fruehere lineinfile-Tasks (bind_host, bind_port und zwei # Fehlversuche fuer die Protokollierstufe). lineinfile haengt neue Zeilen # ans Dateiende und damit in den zuletzt geoeffneten INI-Abschnitt -- bei # guacd.conf, wo jeder Parameter in genau einen Abschnitt gehoert, fuehrt # das verlaesslich zu einem Startabbruch von guacd. Siehe Kopfkommentar # der Vorlage. ansible.builtin.template: src: guacd.conf.j2 dest: /etc/guacamole/guacd.conf owner: root group: root mode: "0644" # Vor dem Ueberschreiben eine Sicherung anlegen: die Datei koennte auf # einem bestehenden Host von Hand angepasst worden sein. backup: true notify: restart guacd - name: TLS-Kompatibilitaetskonfiguration fuer guacd ausrollen (nur wenn eingeschaltet) # Greift ausschliesslich ueber Environment=OPENSSL_CONF in der guacd-Unit # und damit nur fuer diesen einen Prozess -- siehe Kopfkommentar der # Vorlage. Standardmaessig ausgeschaltet. ansible.builtin.template: src: openssl-guacd.cnf.j2 dest: /etc/guacamole/openssl-guacd.cnf owner: root group: root mode: "0644" when: guacd_tls_legacy_compat | bool notify: restart guacd - name: TLS-Kompatibilitaetskonfiguration wieder entfernen, wenn abgeschaltet # Damit ein Zurueckstellen von guacd_tls_legacy_compat auf false die # Lockerung auch tatsaechlich zurueckdreht, statt eine verwaiste Datei # liegen zu lassen, auf die dann nur noch die Unit nicht mehr zeigt. ansible.builtin.file: path: /etc/guacamole/openssl-guacd.cnf state: absent when: not (guacd_tls_legacy_compat | bool) notify: restart guacd # --- OpenSSL-Sicherheitsstufe fuer FreeRDP (Befund 3.9., siehe files/*.c) --- # # Nur aktiv, wenn guacd_tls_seclevel gesetzt ist. Standardmaessig leer. - name: Uebersetzer fuer die Sicherheitsstufen-Zwischenschicht sicherstellen # Auf dem Quellcode-Pfad ist gcc ueber build-essential ohnehin da; auf dem # apt-Pfad (Ubuntu) nicht. Die Zwischenschicht bindet bewusst KEINE # OpenSSL-Header ein, deshalb reicht gcc -- kein libssl-dev noetig. ansible.builtin.apt: name: gcc state: present when: guacd_tls_seclevel | string | length > 0 - name: Quelle der Sicherheitsstufen-Zwischenschicht ablegen ansible.builtin.copy: src: guacd_tls_seclevel_shim.c dest: /usr/local/src/guacd_tls_seclevel_shim.c owner: root group: root mode: "0644" when: guacd_tls_seclevel | string | length > 0 register: _shim_quelle - name: Verzeichnis fuer die uebersetzte Zwischenschicht anlegen ansible.builtin.file: path: /usr/local/lib/jumphost state: directory owner: root group: root mode: "0755" when: guacd_tls_seclevel | string | length > 0 - name: Sicherheitsstufen-Zwischenschicht uebersetzen ansible.builtin.command: cmd: >- gcc -shared -fPIC -O2 -Wall -Wextra -o /usr/local/lib/jumphost/guacd-tls-seclevel-shim.so /usr/local/src/guacd_tls_seclevel_shim.c -ldl when: guacd_tls_seclevel | string | length > 0 register: _shim_build changed_when: true notify: restart guacd - name: Pruefen, ob FreeRDP die OpenSSL-Sicherheitsstufe tatsaechlich selbst setzt # Die Zwischenschicht kann nur wirken, wenn FreeRDP # SSL_CTX_set_security_level dynamisch aufruft. Taete es das nicht, waere # sie wirkungslos -- und das soll auffallen, statt dass jemand spaeter # raetselt, warum die Einstellung nichts bewirkt. ansible.builtin.shell: | set -o pipefail lib=$(ldconfig -p | awk '/libfreerdp[0-9]*\.so/ {print $NF; exit}') if [ -z "$lib" ]; then echo "KEINE_LIB"; exit 0; fi if grep -qa 'SSL_CTX_set_security_level' "$lib"; then echo "JA $lib"; else echo "NEIN $lib"; fi args: executable: /bin/bash register: _freerdp_seclevel changed_when: false check_mode: false when: guacd_tls_seclevel | string | length > 0 - name: Abbrechen, wenn die Zwischenschicht wirkungslos waere ansible.builtin.fail: msg: >- guacd_tls_seclevel ist gesetzt, aber die gefundene FreeRDP-Bibliothek ruft SSL_CTX_set_security_level nicht auf ({{ _freerdp_seclevel.stdout }}). Die Zwischenschicht bliebe wirkungslos. Entweder guacd_tls_seclevel wieder leeren, oder die Ursache erneut mit scripts/diagnose_rdp_tls.py eingrenzen. when: - guacd_tls_seclevel | string | length > 0 - _freerdp_seclevel.stdout is defined - _freerdp_seclevel.stdout is not match('^JA ') - name: Sicherheitsstufen-Zwischenschicht wieder entfernen, wenn abgeschaltet # Damit ein Leeren von guacd_tls_seclevel die Absenkung auch tatsaechlich # zurueckdreht und keine verwaiste Bibliothek liegen bleibt. ansible.builtin.file: path: "{{ item }}" state: absent loop: - /usr/local/lib/jumphost/guacd-tls-seclevel-shim.so - /usr/local/src/guacd_tls_seclevel_shim.c when: guacd_tls_seclevel | string | length == 0 notify: restart guacd - name: Override-Verzeichnis fuer guacd-Unit anlegen ansible.builtin.file: path: /etc/systemd/system/guacd.service.d state: directory owner: root group: root mode: "0755" - name: Gehaertete systemd-Unit fuer guacd ausrollen (Override) ansible.builtin.template: src: guacd.service.j2 dest: /etc/systemd/system/guacd.service.d/override.conf owner: root group: root mode: "0644" notify: - reload systemd - restart guacd # --- Start-/Erreichbarkeitspruefung (Befunde 1, 2, 7 aus Umsetzungsauftrag_Sonnet5.md Teil A.2) --- - name: Anstehende Handler (z.B. "restart guacd" nach Config-/Unit-Aenderung) sofort anwenden # Befund 7: Handler werden von Ansible standardmaessig erst am ENDE DES # GESAMTEN PLAYS geflusht, nicht am Ende dieser Rolle. Ohne dieses meta # wuerde "restart guacd" erst NACH dem ersten Start von jumphost_app # laufen (site.yml ordnet die Rollen zwar guacd -> jumphost_app, aber der # Handler-Flush ist rollenuebergreifend ans Play-Ende gebunden) -- der # allererste App-Start faende dann einen noch alten/toten guacd-Prozess vor. ansible.builtin.meta: flush_handlers - name: guacd aktivieren und starten (unabhaengig davon, ob gerade ein Handler ausgeloest wurde) # Befund 1: die Rolle hatte bislang KEINEN expliziten service-Task, nur # den Handler "restart guacd" (laeuft ausschliesslich bei einer # Aenderung). Ein zweiter Ansible-Lauf ohne Aenderung meldete bisher # ueberall "ok", auch wenn guacd zwischenzeitlich (Absturz, manuelles # "systemctl stop", OOM-Kill) tot war -- das ist nach dem Debian-12-Wechsel # die wahrscheinlichste verbleibende Ursache fuer # "[Errno 111] Connection refused" auf einem sonst funktionierenden Build. ansible.builtin.service: name: guacd state: started enabled: true - name: Warten, bis guacd tatsaechlich auf dem konfigurierten Port lauscht # Befund 2: keine Startpruefung. Ohne wait_for bleibt ein fehlgeschlagener # Start unbemerkt, bis der erste Benutzer eine RDP-Sitzung versucht -- # genau das Symptom aus dem gemeldeten Vorfall (Teil A.0). ansible.builtin.wait_for: host: 127.0.0.1 port: "{{ guacd_port }}" timeout: 15 state: started register: _guacd_port_check ignore_errors: true - name: Abbrechen mit Klartext, wenn guacd nicht auf dem konfigurierten Port lauscht ansible.builtin.fail: msg: >- guacd lauscht nach dem Start NICHT auf 127.0.0.1:{{ guacd_port }} (Timeout nach 15s). Auf dem Zielsystem pruefen: 'systemctl status guacd', 'journalctl -u guacd -n 50', 'ss -ltnp | grep {{ guacd_port }}'. Haeufigste Ursachen laut Umsetzungsauftrag_Sonnet5.md Teil A.2: das systemd-Hardening-Drop-in (ProtectSystem=strict) blockiert ein von guacd benoetigtes Laufzeit-/Schreibverzeichnis, oder /etc/guacamole/guacd.conf wird wegen eines fehlenden --sysconfdir=/etc beim configure-Lauf gar nicht erst gelesen. when: _guacd_port_check is failed - name: Smoke-Test - lauschenden Port gegen die App-Konfiguration abgleichen # Letzter Punkt aus der "Zu tun"-Liste (Teil A.2): der oben erfolgreich # geprüfte Port ist bereits exakt derselbe Wert wie guacd_port, aus dem # sowohl diese Rolle als auch app/config.py::settings.guacd_port (ueber # die Rolle jumphost_app) gespeist werden -- ein Auseinanderlaufen ist # durch die gemeinsame Variable strukturell ausgeschlossen. Trotzdem # explizit sichtbar machen, damit ein kuenftiger Review sofort sieht, WO # dieser Abgleich stattfindet, falls App- und guacd-Port je entkoppelt # werden sollten. ansible.builtin.debug: msg: >- guacd-Smoke-Test erfolgreich: guacd lauscht auf 127.0.0.1:{{ guacd_port }}, identisch mit dem von der App erwarteten Port (Variable guacd_port, siehe app/config.py::settings.guacd_port).