umbau 1.3.2

This commit is contained in:
2026-09-03 10:34:01 +02:00
parent b2c72de069
commit 0728a9af27
7 changed files with 392 additions and 53 deletions

View File

@ -66,3 +66,29 @@ guacd_tls_min_protocol: "TLSv1"
guacd_tls_max_protocol: ""
guacd_tls_security_level: 0
guacd_tls_unsafe_legacy_renegotiation: true
# Leer = OpenSSL-Vorgabe. Nur setzen, wenn das Ziel ein bestimmtes
# Signaturverfahren braucht, z.B.:
# "RSA+SHA256:RSA+SHA384:RSA+SHA512:ECDSA+SHA256:RSA+SHA1:ECDSA+SHA1"
# Wirkt nur zusammen mit guacd_tls_security_level: 0 -- die
# Sicherheitsstufe filtert die Liste anschliessend nochmals.
guacd_tls_signature_algorithms: ""
# OpenSSL-Sicherheitsstufe, die FreeRDP fuer RDP-Verbindungen verwenden soll.
# Leer = nicht eingreifen (FreeRDP-Vorgabe 1).
#
# Gemessen am 3.9. gegen 10.0.0.12: Stufe 2 UND Stufe 1 werden vom Ziel mit
# einem TCP-RST beantwortet, erst Stufe 0 kommt durch (TLS 1.2,
# ECDHE-RSA-AES256-SHA384). Die Cipher-Listen sind auf allen Stufen identisch;
# der Unterschied ist die signature_algorithms-Erweiterung, in der OpenSSL 3.0
# ab Stufe 1 kein SHA-1 mehr anbietet.
#
# ACHTUNG -- das laesst sich NICHT ueber OPENSSL_CONF loesen: FreeRDP ruft
# SSL_CTX_set_security_level() selbst auf und ueberschreibt die Datei-Vorgabe.
# Deshalb die Zwischenschicht in files/guacd_tls_seclevel_shim.c, die nur im
# guacd-Prozess geladen wird. Sie gilt fuer ALLE RDP-Ziele dieses Jumphosts,
# nicht je Host.
#
# Zusammen mit guacd_tls_legacy_compat verwenden (MinProtocol/MaxProtocol);
# beide zusammen ergeben genau die Kombination, die das Diagnoseskript als
# funktionierend gemessen hat.
guacd_tls_seclevel: ""

View File

@ -0,0 +1,126 @@
/*
* guacd-tls-seclevel-shim -- setzt die OpenSSL-Sicherheitsstufe, die FreeRDP
* fuer seine RDP-Verbindungen verwendet.
*
* WOZU (Live-Test 3.9.2026, jumphost-97 -> 10.0.0.12)
* ---------------------------------------------------
* RDP-Verbindungen scheiterten mit
* ERRCONNECT_TLS_CONNECT_FAILED [0x00020008]
* und guacds daraus abgeleiteter Meldung
* "SSL/TLS connection failed (untrusted/self-signed certificate?)".
* Der Klammerzusatz ist irrefuehrend -- guacd setzt diesen festen Text fuer
* genau diesen FreeRDP-Fehlercode. Am Zertifikat lag es nicht: im Journal
* fehlte jede "Certificate validation ..."-Zeile, der Zertifikats-Rueckruf
* wurde also nie erreicht.
*
* Gemessen mit scripts/diagnose_rdp_tls.py:
* OpenSSL-Sicherheitsstufe 2 -> Verbindung vom Ziel zurueckgesetzt (TCP-RST)
* OpenSSL-Sicherheitsstufe 1 -> IDENTISCH zurueckgesetzt
* OpenSSL-Sicherheitsstufe 0 -> erfolgreich, TLS 1.2, ECDHE-RSA-AES256-SHA384
*
* Die Cipher-Listen sind auf allen drei Stufen identisch (gegengeprueft mit
* `openssl ciphers -s 'DEFAULT@SECLEVEL=0|1|2'` -- jeweils dieselben 30). Der
* Unterschied liegt also in der signature_algorithms-Erweiterung: OpenSSL 3.0
* bietet SHA-1 ab Stufe 1 nicht mehr als Signaturverfahren an. Das Windows-Ziel
* kann den ServerKeyExchange offenbar nicht anders signieren, findet nichts
* Verwendbares und legt auf -- SCHANNEL schickt dabei ein TCP-RST statt einer
* sauberen handshake_failure-Warnung.
*
* WARUM NICHT EINFACH OPENSSL_CONF
* --------------------------------
* Wurde versucht und reicht nicht. FreeRDP 2 setzt die Sicherheitsstufe SELBST
* (Kommandozeilenoption /tls-seclevel, Vorgabe 1) und ruft dafuer nach dem
* Anlegen des SSL_CTX SSL_CTX_set_security_level() auf. Das ueberschreibt jede
* Vorgabe aus /etc/ssl/openssl.cnf bzw. aus einer eigenen OPENSSL_CONF.
* guacamole-server 1.5.5 reicht diese FreeRDP-Option nicht als RDP-Parameter
* durch -- es gibt also keinen Konfigurationsweg dorthin.
*
* WARUM DIESER WEG UND NICHT EIN QUELLCODE-PATCH
* ----------------------------------------------
* Ein Patch an guacamole-server muesste das Argument-Array GUAC_RDP_CLIENT_ARGS
* und das dazu positionsgleiche IDX-Enum in src/protocols/rdp/settings.c
* gemeinsam erweitern. Das ist bei jedem Versionswechsel neu anzupassen und
* bricht den Build, sobald sich dort etwas verschiebt -- auf einem Host, der
* guacd bei jedem Ansible-Lauf aus dem Quellcode baut, ein schlechtes Geschaeft.
* Diese Zwischenschicht ist dagegen versionsunabhaengig, kann den Build nicht
* beschaedigen und laesst sich durch Entfernen einer Environment-Zeile in der
* guacd-Unit vollstaendig zuruecknehmen.
*
* VERHALTEN
* ---------
* Ohne die Umgebungsvariable JUMPHOST_GUACD_TLS_SECLEVEL tut diese Bibliothek
* NICHTS: sie reicht jeden Aufruf unveraendert weiter. Erst ein gesetzter,
* nicht-negativer Wert erzwingt diese Stufe. Die erste tatsaechliche Aenderung
* wird nach syslog gemeldet, damit im Journal nachvollziehbar steht, dass hier
* etwas eingreift.
*
* REICHWEITE
* ----------
* Wird ausschliesslich ueber Environment=LD_PRELOAD= in der guacd-Unit geladen
* und wirkt damit nur im guacd-Prozess. Da guacd auf diesem Jumphost nur RDP
* spricht und selbst kein TLS anbietet (bind_host=127.0.0.1, kein [ssl]-
* Abschnitt in guacd.conf), betrifft die Absenkung genau die Strecke
* guacd -> Windows-Ziel. Browser -> Jumphost, nginx, die Anwendung, SSH und
* apt bleiben unberuehrt.
*
* EINSCHRAENKUNG
* --------------
* Die Stufe gilt fuer ALLE RDP-Ziele dieses Jumphosts, nicht je Host. Eine
* Einstellung je Host waere nur ueber den oben verworfenen Quellcode-Patch zu
* haben.
*
* Bewusst ohne <openssl/ssl.h>: der SSL_CTX-Zeiger wird nur durchgereicht, nie
* dereferenziert. Damit braucht das Uebersetzen nur gcc, kein libssl-dev.
*/
#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdlib.h>
#include <syslog.h>
typedef struct ssl_ctx_st SSL_CTX;
static int gewuenschte_stufe = -1; /* -1 = nicht eingreifen */
static int stufe_gelesen = 0;
static int bereits_gemeldet = 0;
static void stufe_lesen(void) {
const char* wert = getenv("JUMPHOST_GUACD_TLS_SECLEVEL");
if (wert != NULL && *wert != '\0') {
char* ende = NULL;
long gelesen = strtol(wert, &ende, 10);
/* Nur vollstaendig geparste Werte im gueltigen OpenSSL-Bereich 0..5
* uebernehmen. Bei Unsinn in der Variablen lieber nicht eingreifen,
* als eine unbeabsichtigte Stufe zu erzwingen. */
if (ende != NULL && *ende == '\0' && gelesen >= 0 && gelesen <= 5)
gewuenschte_stufe = (int) gelesen;
else
syslog(LOG_WARNING, "guacd-tls-seclevel-shim: unbrauchbarer Wert "
"JUMPHOST_GUACD_TLS_SECLEVEL=\"%s\", bleibe untaetig", wert);
}
stufe_gelesen = 1;
}
void SSL_CTX_set_security_level(SSL_CTX* ctx, int stufe) {
static void (*original)(SSL_CTX*, int) = NULL;
if (original == NULL)
original = (void (*)(SSL_CTX*, int)) dlsym(RTLD_NEXT, "SSL_CTX_set_security_level");
if (!stufe_gelesen)
stufe_lesen();
if (gewuenschte_stufe >= 0 && stufe != gewuenschte_stufe) {
if (!bereits_gemeldet) {
syslog(LOG_INFO, "guacd-tls-seclevel-shim: OpenSSL-Sicherheitsstufe %d -> %d "
"(siehe ansible/roles/guacd, guacd_tls_seclevel)",
stufe, gewuenschte_stufe);
bereits_gemeldet = 1;
}
stufe = gewuenschte_stufe;
}
/* Fehlt das Original wider Erwarten, ist Nichtstun richtig: OpenSSL bleibt
* dann bei seiner Vorgabe, statt dass der Aufruf ins Leere laeuft. */
if (original != NULL)
original(ctx, stufe);
}

View File

@ -255,59 +255,24 @@
group: root
mode: "0755"
- name: guacd nur an localhost binden
ansible.builtin.lineinfile:
path: /etc/guacamole/guacd.conf
regexp: '^bind_host'
line: "bind_host = 127.0.0.1"
create: true
- 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: guacd-Port setzen
ansible.builtin.lineinfile:
path: /etc/guacamole/guacd.conf
regexp: '^bind_port'
line: "bind_port = {{ guacd_port }}"
create: true
notify: restart guacd
- name: Falsch benannte Protokollierstufe aus guacd.conf entfernen
# BUGFIX (3.9.): hier stand zuerst "log_level = ...". Diesen Parameter
# kennt guacd.conf NICHT -- er heisst "max_log_level" (guacd.conf(5):
# pid_file, max_log_level, bind_host, bind_port, server_certificate,
# server_key). guacd bricht bei einem unbekannten Schluessel den Start
# komplett ab ("Parse error at line N: Invalid parameter or section name"),
# es gibt also keine stille Fehlbedienung -- der Dienst startete gar nicht
# mehr. Diese Aufraeumzeile entfernt die falsche Zeile auf Hosts, auf denen
# die fehlerhafte Fassung bereits ausgerollt wurde.
ansible.builtin.lineinfile:
path: /etc/guacamole/guacd.conf
regexp: '^\s*log_level\s*='
state: absent
notify: restart guacd
- name: guacd-Protokollierstufe setzen
# Auf "info" (Standard) protokolliert guacd bereits, ob die
# Zertifikatspruefung wegen ignore-cert uebersprungen wurde
# ("Certificate validation bypassed") oder fehlgeschlagen ist -- die
# entscheidende Unterscheidung bei "Ziel nicht erreichbar (Code 519):
# SSL/TLS connection failed". Fuer den konkreten FreeRDP-/OpenSSL-Fehler
# guacd_log_level voruebergehend auf "debug" setzen (siehe defaults).
ansible.builtin.lineinfile:
path: /etc/guacamole/guacd.conf
regexp: '^\s*max_log_level\s*='
line: "max_log_level = {{ guacd_log_level }}"
create: true
notify: restart guacd
- name: RDP-Laufwerksumleitungs-Verzeichnisse anlegen (Filetransfer, Konzept 4.3)
ansible.builtin.file:
path: "/var/lib/jumphost/rdp-drives"
state: directory
owner: guacd
group: guacd
mode: "0750"
- 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
@ -331,6 +296,91 @@
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

View File

@ -0,0 +1,30 @@
# /etc/guacamole/guacd.conf -- von Ansible verwaltet (Rolle "guacd").
#
# WICHTIG, zweimal falsch gemacht am 3.9.: guacd.conf ist eine INI-Datei mit
# FESTEN Abschnitten, und jeder Parameter gehoert in genau einen davon.
# guacd bricht bei einem Parameter im falschen Abschnitt den START ab
# ("Parse error at line N, column 1: Invalid parameter or section name") --
# der Dienst laeuft dann gar nicht mehr.
#
# [daemon] pid_file, log_level
# [server] bind_host, bind_port
# [ssl] server_certificate, server_key
#
# "log_level" gehoert also unter [daemon], NICHT unter [server]. Und es heisst
# "log_level" -- "max_log_level" gibt es nicht (das war der zweite Fehlversuch).
#
# Deshalb jetzt eine vollstaendige Vorlage statt einzelner lineinfile-Eintraege:
# lineinfile haengt eine Zeile ans DATEIENDE, also in den zuletzt geoeffneten
# Abschnitt, ohne den Zusammenhang zu kennen. Fuer eine abschnittsbasierte
# Konfigurationsdatei ist das die falsche Werkzeugwahl.
[daemon]
# trace | debug | info | warning | error
# "info" protokolliert bereits "Certificate validation bypassed" bzw.
# "... failed" -- die entscheidende Unterscheidung bei RDP-Fehler 519.
# Fuer den konkreten FreeRDP-/OpenSSL-Fehler voruebergehend "debug".
log_level = {{ guacd_log_level }}
[server]
bind_host = 127.0.0.1
bind_port = {{ guacd_port }}

View File

@ -7,6 +7,16 @@
[Service]
User=guacd
Group=guacd
{% if guacd_tls_seclevel | string | length > 0 %}
# OpenSSL-Sicherheitsstufe fuer FreeRDP erzwingen (Befund 3.9.). FreeRDP setzt
# sie selbst (Vorgabe 1) und ueberschreibt damit jede OPENSSL_CONF-Vorgabe;
# guacamole-server reicht die zustaendige FreeRDP-Option nicht durch. Die
# Zwischenschicht greift NUR in diesem Prozess und nur, solange
# JUMPHOST_GUACD_TLS_SECLEVEL gesetzt ist -- ausfuehrliche Begruendung im
# Kopfkommentar von roles/guacd/files/guacd_tls_seclevel_shim.c.
Environment=LD_PRELOAD=/usr/local/lib/jumphost/guacd-tls-seclevel-shim.so
Environment=JUMPHOST_GUACD_TLS_SECLEVEL={{ guacd_tls_seclevel }}
{% endif %}
{% if guacd_tls_legacy_compat %}
# TLS-Kompatibilitaet fuer die Strecke guacd -> Windows-Ziel (Live-Test 3.9.).
# Setzt die OpenSSL-Vorgaben NUR fuer diesen Prozess; systemweit bleibt

View File

@ -15,6 +15,14 @@
# diesem Fall irrefuehrend; die Hosteinstellung "Zertifikat ignorieren"
# kann daran folglich nichts aendern.
#
# Gemessener Fall (jumphost-97 -> 10.0.0.12, 3.9.): der Handshake gelingt erst
# ab Sicherheitsstufe 0; Stufe 1 UND 2 scheitern beide gleich mit einem TCP-RST
# der Gegenstelle. Das deutet auf die signature_algorithms-Erweiterung: OpenSSL
# 3.0 bietet ab Stufe 1 kein SHA-1 mehr als Signaturverfahren an, und das Ziel
# kann den ServerKeyExchange offenbar nicht anders signieren. Nicht das
# ZERTIFIKAT ist das Problem -- dessen Signatur prueft bei ignore-cert ohnehin
# niemand (eigens gegengeprueft).
#
# ABWAEGUNG (bewusst zu treffen, deshalb standardmaessig AUS):
# Diese Datei senkt die TLS-Anforderungen fuer die Strecke
# Jumphost -> Windows-Ziel. Die Strecke Browser -> Jumphost ist davon nicht
@ -22,6 +30,9 @@
# vertretbar; die saubere Loesung bleibt, TLS 1.2 auf dem Windows-Ziel zu
# aktivieren. Nur so weit lockern wie noetig -- guacd_tls_min_protocol und
# guacd_tls_security_level einzeln nachziehen, statt pauschal auf TLSv1/0.
# Auch bei SECLEVEL=0 schliesst 'DEFAULT' weiterhin NULL- und anonyme Cipher
# aus -- eine ungeschuetzte Verbindung wird dadurch also nicht moeglich.
# Zusammen mit MinProtocol=MaxProtocol=TLSv1.2 bleibt die Lockerung eng.
openssl_conf = default_conf
@ -37,6 +48,16 @@ MinProtocol = {{ guacd_tls_min_protocol }}
MaxProtocol = {{ guacd_tls_max_protocol }}
{% endif %}
CipherString = DEFAULT@SECLEVEL={{ guacd_tls_security_level }}
{% if guacd_tls_signature_algorithms | default('', true) | length > 0 %}
# Explizite Liste der angebotenen Signaturverfahren. Gemessen am 3.9.:
# die Cipher-Liste ist auf allen Sicherheitsstufen IDENTISCH
# (openssl ciphers -s 'DEFAULT@SECLEVEL=0|1|2' -> jeweils dieselben 30),
# der Unterschied liegt also in der signature_algorithms-Erweiterung:
# OpenSSL 3.0 bietet SHA-1 ab Stufe 1 nicht mehr an. Achtung -- die
# Sicherheitsstufe filtert diese Liste NACHTRAEGLICH nochmals, ohne
# guacd_tls_security_level: 0 bleibt SHA-1 also trotzdem draussen.
SignatureAlgorithms = {{ guacd_tls_signature_algorithms }}
{% endif %}
{% if guacd_tls_unsafe_legacy_renegotiation %}
# Manche aeltere Windows-/SCHANNEL-Staende koennen die in OpenSSL 3
# standardmaessig abgeschaltete unsichere Neuverhandlung verlangen.