umbau 1.3.1
This commit is contained in:
0
_to_delete/git-index.lock.1788415639
Normal file
0
_to_delete/git-index.lock.1788415639
Normal file
@ -39,3 +39,25 @@ guacd_git_ref: "main"
|
||||
# zuruecksetzen: "debug"/"trace" sind gespraechig und koennen Details der
|
||||
# Zielumgebung ins Journal schreiben.
|
||||
guacd_log_level: "info"
|
||||
|
||||
# --- TLS-Kompatibilitaet guacd -> Windows-Ziel (Live-Test 3.9.) -------------
|
||||
#
|
||||
# Standardmaessig AUS: guacd erbt dann die strengen systemweiten
|
||||
# OpenSSL-Vorgaben von Debian 12 (MinProtocol=TLSv1.2, DEFAULT@SECLEVEL=2).
|
||||
#
|
||||
# Auf true setzen, wenn scripts/diagnose_rdp_tls.py den Befund
|
||||
# "scheitert NUR an den OpenSSL-Vorgaben dieses Jumphosts" liefert -- also
|
||||
# der TLS-Handshake zum Windows-Ziel mit den System-Vorgaben scheitert, mit
|
||||
# gelockerten aber gelingt. Symptom in der Oberflaeche:
|
||||
# "Ziel nicht erreichbar (Code 519): SSL/TLS connection failed
|
||||
# (untrusted/self-signed certificate?)", OHNE dass im guacd-Journal eine
|
||||
# "Certificate validation ..."-Zeile steht.
|
||||
#
|
||||
# Wirkt NUR auf den guacd-Prozess (Environment=OPENSSL_CONF in dessen Unit),
|
||||
# nicht systemweit. Immer nur so weit lockern wie noetig: das Diagnoseskript
|
||||
# nennt die tatsaechlich vom Ziel gesprochene TLS-Version, danach
|
||||
# guacd_tls_min_protocol genau darauf setzen statt pauschal auf TLSv1.
|
||||
guacd_tls_legacy_compat: false
|
||||
guacd_tls_min_protocol: "TLSv1"
|
||||
guacd_tls_security_level: 0
|
||||
guacd_tls_unsafe_legacy_renegotiation: true
|
||||
|
||||
@ -293,6 +293,29 @@
|
||||
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
|
||||
# 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
|
||||
|
||||
- name: Override-Verzeichnis fuer guacd-Unit anlegen
|
||||
ansible.builtin.file:
|
||||
path: /etc/systemd/system/guacd.service.d
|
||||
|
||||
@ -7,6 +7,13 @@
|
||||
[Service]
|
||||
User=guacd
|
||||
Group=guacd
|
||||
{% 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
|
||||
# /etc/ssl/openssl.cnf unberuehrt. Siehe openssl-guacd.cnf.j2 und die
|
||||
# Variablen guacd_tls_* in defaults/main.yml.
|
||||
Environment=OPENSSL_CONF=/etc/guacamole/openssl-guacd.cnf
|
||||
{% endif %}
|
||||
NoNewPrivileges=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=true
|
||||
|
||||
41
ansible/roles/guacd/templates/openssl-guacd.cnf.j2
Normal file
41
ansible/roles/guacd/templates/openssl-guacd.cnf.j2
Normal file
@ -0,0 +1,41 @@
|
||||
# OpenSSL-Konfiguration AUSSCHLIESSLICH fuer den guacd-Prozess.
|
||||
#
|
||||
# Wird ueber Environment=OPENSSL_CONF= in der guacd-Unit gesetzt (siehe
|
||||
# guacd.service.j2) und gilt damit NUR fuer guacd/FreeRDP -- nicht fuer
|
||||
# nginx, nicht fuer die Jumphost-App, nicht fuer SSH, nicht fuer apt.
|
||||
# Systemweit (/etc/ssl/openssl.cnf) bleibt alles unveraendert streng.
|
||||
#
|
||||
# Wozu: Debian 12 setzt systemweit MinProtocol = TLSv1.2 und
|
||||
# CipherString = DEFAULT@SECLEVEL=2. Ein Windows-Ziel, dessen SCHANNEL-
|
||||
# Konfiguration nur aeltere TLS-Versionen oder schwaechere Cipher anbietet,
|
||||
# ist damit von diesem Host aus NICHT erreichbar: OpenSSL bricht den
|
||||
# Handshake ab, BEVOR ueberhaupt ein Zertifikat geprueft wird. guacd meldet
|
||||
# das als "SSL/TLS connection failed (untrusted/self-signed certificate?)"
|
||||
# -- der Klammerzusatz ist eine feste Vermutung im guacd-Quelltext und in
|
||||
# diesem Fall irrefuehrend; die Hosteinstellung "Zertifikat ignorieren"
|
||||
# kann daran folglich nichts aendern.
|
||||
#
|
||||
# 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
|
||||
# betroffen. Innerhalb eines abgeschotteten Verwaltungsnetzes ist das
|
||||
# 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.
|
||||
|
||||
openssl_conf = default_conf
|
||||
|
||||
[default_conf]
|
||||
ssl_conf = ssl_sect
|
||||
|
||||
[ssl_sect]
|
||||
system_default = system_default_sect
|
||||
|
||||
[system_default_sect]
|
||||
MinProtocol = {{ guacd_tls_min_protocol }}
|
||||
CipherString = DEFAULT@SECLEVEL={{ guacd_tls_security_level }}
|
||||
{% if guacd_tls_unsafe_legacy_renegotiation %}
|
||||
# Manche aeltere Windows-/SCHANNEL-Staende koennen die in OpenSSL 3
|
||||
# standardmaessig abgeschaltete unsichere Neuverhandlung verlangen.
|
||||
Options = UnsafeLegacyRenegotiation
|
||||
{% endif %}
|
||||
250
scripts/diagnose_rdp_tls.py
Normal file
250
scripts/diagnose_rdp_tls.py
Normal file
@ -0,0 +1,250 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Stellt genau den TLS-Verbindungsaufbau nach, an dem guacd/FreeRDP scheitert.
|
||||
|
||||
Hintergrund (Live-Test 3.9.):
|
||||
|
||||
guacd meldet
|
||||
"RDP server closed/refused connection: SSL/TLS connection failed
|
||||
(untrusted/self-signed certificate?)"
|
||||
und die Anwendung reicht das als "Ziel nicht erreichbar (Code 519)" weiter.
|
||||
|
||||
Der Klammerzusatz ist eine VERMUTUNG von guacd, kein Befund -- guacd setzt
|
||||
diesen festen Text fuer FreeRDPs Fehlercode ERRCONNECT_TLS_CONNECT_FAILED.
|
||||
Dass es NICHT am Zertifikat liegt, zeigt das guacd-Journal selbst: guacd
|
||||
protokolliert beim Pruefen eines Zertifikats immer eine der beiden Zeilen
|
||||
"Certificate validation bypassed" (ignore-cert greift) oder "Certificate
|
||||
validation failed". Steht KEINE von beiden im Journal, wurde der
|
||||
Zertifikats-Rueckruf nie erreicht -- der Handshake ist also schon vorher
|
||||
gescheitert, bei der Aushandlung von Protokollversion und Cipher. Die
|
||||
Hosteinstellung "Zertifikat ignorieren" kann daran nichts aendern.
|
||||
|
||||
Genau das prueft dieses Skript. Es macht, was FreeRDP macht:
|
||||
|
||||
1. TCP zu <ziel>:3389
|
||||
2. X.224-Verbindungsanfrage mit RDP_NEG_REQ (MS-RDPBCGR 2.2.1.1) --
|
||||
das ist die RDP-eigene Vorstufe, ohne die kein RDP-Server TLS spricht.
|
||||
Deshalb funktioniert "openssl s_client -connect ziel:3389" hier auch
|
||||
nicht: es faengt sofort mit ClientHello an und laeuft ins Leere.
|
||||
3. Auswerten, welche Sicherheitsstufe der Server auswaehlt
|
||||
4. TLS-Handshake auf derselben Verbindung -- und zwar MEHRFACH, mit
|
||||
verschiedenen OpenSSL-Vorgaben:
|
||||
a) System-Standard: exakt das, was guacd/FreeRDP auf diesem Host
|
||||
bekommen. Debian 12 setzt in /etc/ssl/openssl.cnf
|
||||
MinProtocol = TLSv1.2 und CipherString = DEFAULT@SECLEVEL=2.
|
||||
b) Alles erlaubt (SECLEVEL=0, ab TLS 1.0)
|
||||
c/d) TLS 1.0 bzw. TLS 1.2 einzeln erzwungen
|
||||
Der Vergleich sagt, ob die OpenSSL-Vorgaben des JUMPHOSTS die Ursache
|
||||
sind (a scheitert, b klappt) oder das Ziel selbst (alles scheitert).
|
||||
|
||||
Braucht nichts ausser Python 3 -- kein X11, kein xfreerdp, kein nmap.
|
||||
Aufruf: python3 scripts/diagnose_rdp_tls.py 10.0.0.12
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import socket
|
||||
import ssl
|
||||
import struct
|
||||
import sys
|
||||
|
||||
# MS-RDPBCGR 2.2.1.1.1 -- requestedProtocols
|
||||
PROTOCOL_RDP = 0x00000000 # "Native RDP", ohne TLS
|
||||
PROTOCOL_SSL = 0x00000001 # TLS
|
||||
PROTOCOL_HYBRID = 0x00000002 # CredSSP/NLA (setzt TLS voraus)
|
||||
PROTOCOL_RDSTLS = 0x00000004
|
||||
PROTOCOL_HYBRID_EX = 0x00000008
|
||||
|
||||
_PROTOCOL_NAMES = {
|
||||
PROTOCOL_RDP: "Native RDP (ohne TLS)",
|
||||
PROTOCOL_SSL: "TLS",
|
||||
PROTOCOL_HYBRID: "CredSSP/NLA (ueber TLS)",
|
||||
PROTOCOL_RDSTLS: "RDSTLS",
|
||||
PROTOCOL_HYBRID_EX: "CredSSP Early User Auth",
|
||||
}
|
||||
|
||||
# MS-RDPBCGR 2.2.1.2.2 -- failureCode im RDP_NEG_FAILURE
|
||||
_FAILURE_CODES = {
|
||||
0x01: "SSL_REQUIRED_BY_SERVER -- der Server verlangt TLS, es wurde keines angeboten",
|
||||
0x02: "SSL_NOT_ALLOWED_BY_SERVER -- der Server erlaubt kein TLS (Sicherheitsebene 'RDP')",
|
||||
0x03: "SSL_CERT_NOT_ON_SERVER -- der Server hat kein Zertifikat fuer TLS",
|
||||
0x04: "INCONSISTENT_FLAGS",
|
||||
0x05: "HYBRID_REQUIRED_BY_SERVER -- der Server verlangt NLA",
|
||||
0x06: "SSL_WITH_USER_AUTH_REQUIRED_BY_SERVER",
|
||||
}
|
||||
|
||||
|
||||
def _x224_connection_request(requested: int) -> bytes:
|
||||
"""TPKT + X.224 Connection Request mit angehaengtem RDP_NEG_REQ."""
|
||||
neg_req = struct.pack("<BBHI", 0x01, 0x00, 8, requested)
|
||||
# X.224 CR: LI, CR-CDT(0xE0), DST-REF(2), SRC-REF(2), CLASS(1)
|
||||
x224 = struct.pack(">BBHHB", 6 + len(neg_req), 0xE0, 0, 0, 0) + neg_req
|
||||
return struct.pack(">BBH", 0x03, 0x00, 4 + len(x224)) + x224
|
||||
|
||||
|
||||
def _read_tpkt(sock: socket.socket) -> bytes:
|
||||
header = _recv_exact(sock, 4)
|
||||
if header[0] != 0x03:
|
||||
raise ValueError(f"Keine TPKT-Antwort (erstes Byte 0x{header[0]:02x}) -- spricht dort wirklich RDP?")
|
||||
(length,) = struct.unpack(">H", header[2:4])
|
||||
return header + _recv_exact(sock, length - 4)
|
||||
|
||||
|
||||
def _recv_exact(sock: socket.socket, count: int) -> bytes:
|
||||
buf = b""
|
||||
while len(buf) < count:
|
||||
chunk = sock.recv(count - len(buf))
|
||||
if not chunk:
|
||||
raise ConnectionError("Gegenstelle hat die Verbindung waehrend der Aushandlung geschlossen")
|
||||
buf += chunk
|
||||
return buf
|
||||
|
||||
|
||||
def negotiate(sock: socket.socket, requested: int) -> int:
|
||||
"""Fuehrt die X.224-Aushandlung durch, gibt das gewaehlte Protokoll zurueck."""
|
||||
sock.sendall(_x224_connection_request(requested))
|
||||
response = _read_tpkt(sock)
|
||||
body = response[4:]
|
||||
if len(body) < 7:
|
||||
raise ValueError("X.224-Antwort zu kurz")
|
||||
payload = body[7:]
|
||||
if not payload:
|
||||
# Kein RDP_NEG_RSP: aeltere Server antworten so und meinen "Native RDP".
|
||||
return PROTOCOL_RDP
|
||||
kind = payload[0]
|
||||
if kind == 0x03:
|
||||
(code,) = struct.unpack("<I", payload[4:8])
|
||||
raise ValueError(
|
||||
"Der Server LEHNT die gewuenschte Sicherheitsstufe ab: "
|
||||
+ _FAILURE_CODES.get(code, f"unbekannter failureCode 0x{code:02x}")
|
||||
)
|
||||
if kind != 0x02:
|
||||
raise ValueError(f"Unerwartete Antwortart 0x{kind:02x} in der X.224-Antwort")
|
||||
(selected,) = struct.unpack("<I", payload[4:8])
|
||||
return selected
|
||||
|
||||
|
||||
def _context(variant: str) -> ssl.SSLContext:
|
||||
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
||||
# Wie guacd mit ignore-cert: keinerlei Pruefung. Damit ist ausgeschlossen,
|
||||
# dass ein Ergebnis hier an der Zertifikatspruefung haengt.
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
if variant == "system":
|
||||
return ctx # erbt /etc/ssl/openssl.cnf -- genau wie guacd
|
||||
if variant == "relaxed":
|
||||
ctx.minimum_version = ssl.TLSVersion.TLSv1
|
||||
ctx.set_ciphers("ALL:@SECLEVEL=0")
|
||||
return ctx
|
||||
if variant in ("tls1.0", "tls1.1", "tls1.2", "tls1.3"):
|
||||
version = {
|
||||
"tls1.0": ssl.TLSVersion.TLSv1,
|
||||
"tls1.1": ssl.TLSVersion.TLSv1_1,
|
||||
"tls1.2": ssl.TLSVersion.TLSv1_2,
|
||||
"tls1.3": ssl.TLSVersion.TLSv1_3,
|
||||
}[variant]
|
||||
ctx.set_ciphers("ALL:@SECLEVEL=0")
|
||||
ctx.minimum_version = version
|
||||
ctx.maximum_version = version
|
||||
return ctx
|
||||
raise ValueError(variant)
|
||||
|
||||
|
||||
def try_handshake(host: str, port: int, requested: int, variant: str, timeout: float):
|
||||
"""Gibt (erfolg, text) zurueck."""
|
||||
try:
|
||||
with socket.create_connection((host, port), timeout=timeout) as sock:
|
||||
selected = negotiate(sock, requested)
|
||||
if selected == PROTOCOL_RDP and requested != PROTOCOL_RDP:
|
||||
return (False, "Server waehlt 'Native RDP' -- er bietet auf dieser Stufe gar kein TLS an")
|
||||
ctx = _context(variant)
|
||||
with ctx.wrap_socket(sock, server_hostname=host) as tls:
|
||||
cert = tls.getpeercert(binary_form=True)
|
||||
info = f"{tls.version()}, {tls.cipher()[0]}, Zertifikat {len(cert)} Byte"
|
||||
return (True, info)
|
||||
except ssl.SSLError as exc:
|
||||
return (False, f"{type(exc).__name__}: {exc.reason if getattr(exc, 'reason', None) else exc}")
|
||||
except (OSError, ValueError) as exc:
|
||||
return (False, f"{type(exc).__name__}: {exc}")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
parser.add_argument("host", help="Adresse des RDP-Ziels, z.B. 10.0.0.12")
|
||||
parser.add_argument("--port", type=int, default=3389)
|
||||
parser.add_argument("--timeout", type=float, default=10.0)
|
||||
args = parser.parse_args()
|
||||
|
||||
import ssl as _ssl
|
||||
print(f"Jumphost: {_ssl.OPENSSL_VERSION}")
|
||||
try:
|
||||
with open("/etc/ssl/openssl.cnf", encoding="utf-8") as fh:
|
||||
relevant = [
|
||||
line.strip() for line in fh
|
||||
if line.strip().startswith(("MinProtocol", "CipherString", "MaxProtocol", "Options"))
|
||||
]
|
||||
print(" /etc/ssl/openssl.cnf: " + ("; ".join(relevant) if relevant else "keine Protokoll-/Cipher-Vorgaben"))
|
||||
except OSError:
|
||||
print(" /etc/ssl/openssl.cnf nicht lesbar")
|
||||
print(f"Ziel: {args.host}:{args.port}\n")
|
||||
|
||||
# 1) Welche Sicherheitsstufe waehlt der Server, wenn man ihm alles anbietet?
|
||||
print("== Schritt 1: X.224-Aushandlung ==")
|
||||
requested = PROTOCOL_SSL | PROTOCOL_HYBRID
|
||||
try:
|
||||
with socket.create_connection((args.host, args.port), timeout=args.timeout) as sock:
|
||||
selected = negotiate(sock, requested)
|
||||
print(f" Angeboten: TLS + CredSSP/NLA -> Server waehlt: "
|
||||
f"{_PROTOCOL_NAMES.get(selected, hex(selected))}")
|
||||
except Exception as exc:
|
||||
print(f" FEHLGESCHLAGEN: {exc}")
|
||||
print("\n Damit erübrigt sich der TLS-Test -- die RDP-Vorstufe scheitert bereits.")
|
||||
return 1
|
||||
|
||||
if selected == PROTOCOL_RDP:
|
||||
print("\n BEFUND: Das Ziel bietet auf der ausgehandelten Stufe kein TLS an.")
|
||||
print(" Am Windows-Ziel die Sicherheitsebene auf 'Verhandeln' oder 'SSL' stellen")
|
||||
print(" (GPO: 'Bestimmte Sicherheitsebene fuer RDP-Verbindungen anfordern').")
|
||||
return 1
|
||||
|
||||
# 2) TLS-Handshake unter verschiedenen OpenSSL-Vorgaben
|
||||
print("\n== Schritt 2: TLS-Handshake (Zertifikatspruefung ueberall AUS, wie ignore-cert) ==")
|
||||
varianten = [
|
||||
("system", "System-Standard (das, was guacd/FreeRDP bekommt)"),
|
||||
("relaxed", "Alles erlaubt (SECLEVEL=0, ab TLS 1.0)"),
|
||||
("tls1.0", "nur TLS 1.0"),
|
||||
("tls1.1", "nur TLS 1.1"),
|
||||
("tls1.2", "nur TLS 1.2"),
|
||||
("tls1.3", "nur TLS 1.3"),
|
||||
]
|
||||
ergebnisse = {}
|
||||
for variant, label in varianten:
|
||||
ok, text = try_handshake(args.host, args.port, requested, variant, args.timeout)
|
||||
ergebnisse[variant] = ok
|
||||
print(f" [{'OK ' if ok else 'FEHL'}] {label}\n {text}")
|
||||
|
||||
# 3) Auswertung
|
||||
print("\n== Befund ==")
|
||||
if ergebnisse["system"]:
|
||||
print(" Der TLS-Handshake gelingt mit den System-Vorgaben. Die Ursache liegt")
|
||||
print(" dann NICHT in der TLS-Schicht, sondern eine Stufe weiter (CredSSP/NLA,")
|
||||
print(" Anmeldedaten) oder in guacd/FreeRDP selbst.")
|
||||
print(" Naechster Schritt: guacd_log_level auf 'debug' und erneut verbinden.")
|
||||
return 0
|
||||
if ergebnisse["relaxed"]:
|
||||
moeglich = [v for v in ("tls1.0", "tls1.1", "tls1.2", "tls1.3") if ergebnisse[v]]
|
||||
print(" EINDEUTIG: Der Handshake scheitert NUR an den OpenSSL-Vorgaben DIESES")
|
||||
print(" Jumphosts, nicht am Ziel und nicht am Zertifikat.")
|
||||
print(f" Das Ziel spricht: {', '.join(moeglich) if moeglich else '(nur mit SECLEVEL=0)'}")
|
||||
print(" Debian 12 setzt systemweit MinProtocol=TLSv1.2 und DEFAULT@SECLEVEL=2;")
|
||||
print(" guacd erbt das. Abhilfe: eine eigene OPENSSL_CONF NUR fuer die")
|
||||
print(" guacd-Unit (nicht systemweit lockern).")
|
||||
return 1
|
||||
print(" Der Handshake scheitert in JEDER Variante -- die Ursache liegt am Ziel")
|
||||
print(" (10.0.0.12) oder dazwischen, nicht an den OpenSSL-Vorgaben des Jumphosts.")
|
||||
print(" Am Windows-Ziel pruefen: SCHANNEL-Registry (aktivierte TLS-Versionen),")
|
||||
print(" Cipher-Suite-Richtlinie, und ob ein RDP-Zertifikat hinterlegt ist.")
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user