Skip to content

Commit d52cfcd

Browse files
committed
[Server] add XrdSecssh plugin with tests and packaging
Add the new TLS-only XrdSecssh security plugin (sec.protocol ssh) for SSH-key-based authentication, together with its documentation, unit tests and packaging integration. Plugin (src/XrdSecssh/XrdSecProtocolssh.cc): - Raw-key authentication (ssh-ed25519, ssh-rsa) with a server-side keys-file mapping keys to local users (explicit and authorized_keys comment-style mappings). - OpenSSH user certificate authentication validated against a configurable ca-keys-file: signature is verified against a trusted CA, the certificate must be a user certificate (type=1), the validity window is enforced, and any critical option causes a fail-closed rejection. - Principal-to-local-user mapping via -principal-as-user and/or a hot-reloaded -principal-map[-file]. - Two-step challenge/response handshake using a single-use, TTL-bounded random nonce; the signed payload binds the nonce to the presented key/certificate fingerprint. - Client support for private-key files (PEM/PKCS8 ed25519/rsa) and ssh-agent (including certificate identities), with fingerprint-based identity selection. - Hardening: TLS is mandatory; key/CA/principal-map files are loaded via an O_NOFOLLOW, fstat-based safe reader (regular file, owner == euid, not group/other writable, size-capped); RSA uses rsa-sha2-256; pending-challenge state is capped and garbage-collected; and SSH blob length parsing uses 64-bit arithmetic so attacker-supplied 32-bit lengths cannot wrap. - Use RAII wrappers for OpenSSL handles and owning unique_ptr storage for trusted keys and client private keys; allocate protocol/credential objects with make_unique().release() for the XRootD C APIs. - Further hardening: cap per-field SSH wire blobs and keys-file lines; reject duplicate pending challenges per tident; synchronize principal map reload/lookup; require client private keys and ssh-agent sockets to be user-owned and not group/other accessible; validate mapped usernames; and redact fingerprints in debug logs. Documentation (src/XrdSecssh/README.md): - Document the server/client configuration, file formats, option ranges (-maxsz 1..524288, -nonce-ttl 1..600), the file-ownership requirement, startup-only vs. hot-reloaded files, the rsa-sha2-256 requirement, the ssh-agent requirement for client certificate auth, and a security considerations section (TLS-reliance / no channel binding, trusted-CA implications, parser limits, and debug-log redaction). Tests (tests/XrdSec/XrdSecSSH.cc): - GoogleTest unit tests that exercise the plugin internals directly, covering blob/length parsing, the safe file reader, key/CA loading, sign/verify for ed25519 and rsa, the full raw-key and certificate handshakes, principal mapping and hot-reload, and negative cases including untrusted keys/CAs, bad/replayed signatures, expired and not-yet-valid certificates, wrong certificate type, critical options, signature-algorithm mismatch, malformed/truncated inputs, oversized credentials and invalid configuration options. - Add tests for wire-field size limits, duplicate pending challenges, invalid keys-file usernames, client key permission checks, and username validation. Build/packaging: - Register libXrdSecssh.so in XrdVersionPlugin.hh, build it from src/CMakeLists.txt, wire up the tests/XrdSec unit-test target, and add the plugin to the RPM (xrootd.spec), Debian (xrootd-plugins.install) and Python install documentation. Assisted-by: Cursor:Opus-4.8 CursorAI
1 parent e53e01a commit d52cfcd

11 files changed

Lines changed: 4356 additions & 2 deletions

File tree

debian/xrootd-plugins.install

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@
1010
/usr/lib/*/libXrdSecsss-6.so
1111
/usr/lib/*/libXrdSecunix-6.so
1212
/usr/lib/*/libXrdSecztn-6.so
13+
/usr/lib/*/libXrdSecssh-6.so

python/docs/source/install.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ In this case, the structure is a bit different than before::
166166
| |-- libXrdSecgsiAUTHZVO-5.so
167167
| |-- libXrdSecgsiGMAPDN-5.so
168168
| |-- libXrdSeckrb5-5.so
169+
| |-- libXrdSecssh-5.so
169170
| |-- libXrdSecpwd-5.so
170171
| |-- libXrdSecsss-5.so
171172
| |-- libXrdSecunix-5.so

src/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ add_subdirectory( XrdPosix )
6262
add_subdirectory( XrdSec )
6363
add_subdirectory( XrdSecgsi )
6464
add_subdirectory( XrdSeckrb5 )
65+
add_subdirectory( XrdSecssh )
6566
add_subdirectory( XrdSecpwd )
6667
add_subdirectory( XrdSecsss )
6768
add_subdirectory( XrdSecunix )

src/XrdSecssh/CMakeLists.txt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
if( XRDCL_ONLY )
2+
return()
3+
endif()
4+
5+
set(XrdSecssh XrdSecssh-${PLUGIN_VERSION})
6+
7+
find_package(OpenSSL REQUIRED)
8+
9+
add_library(${XrdSecssh} MODULE XrdSecProtocolssh.cc)
10+
target_link_libraries(${XrdSecssh} PRIVATE XrdUtils OpenSSL::Crypto)
11+
12+
install(TARGETS ${XrdSecssh} LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR})

src/XrdSecssh/README.md

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
# XrdSecssh
2+
3+
`XrdSecssh` is an experimental XRootD security protocol plugin (`sec.protocol ssh`)
4+
for SSH-key-based authentication over TLS.
5+
6+
## V1 behavior
7+
8+
- Raw key mode and OpenSSH user certificate mode
9+
- Server trust source:
10+
- `keys-file` for raw key -> user mapping
11+
- optional `ca-keys-file` for user certificate validation
12+
- Supported raw key algorithms: `ssh-ed25519`, `ssh-rsa`
13+
- Supported user certificate algorithms: `ssh-ed25519-cert-v01@openssh.com`,
14+
`ssh-rsa-cert-v01@openssh.com`
15+
- Two-step handshake:
16+
1. client sends user + SSH key/certificate blob
17+
2. server sends nonce challenge
18+
3. client signs challenge with private key
19+
4. server verifies signature and maps to local username
20+
21+
## Server configuration
22+
23+
```conf
24+
sec.protocol ssh \
25+
-keys-file /etc/xrootd/ssh_authorized_keys \
26+
-ca-keys-file /etc/xrootd/ssh_ca_keys \
27+
-principal-as-user \
28+
-principal-map \
29+
-maxsz 8192 \
30+
-nonce-ttl 30 \
31+
-debug
32+
```
33+
34+
`keys-file` security checks:
35+
36+
- opened with `O_NOFOLLOW` (symlinks are rejected)
37+
- must be a regular file
38+
- must be owned by the effective xrootd uid
39+
- must not be group/other writable
40+
- must not exceed 10 MB
41+
42+
The same security checks apply to `ca-keys-file` when configured.
43+
The same security checks apply to `principal-map-file` when configured.
44+
45+
> **Note:** the ownership check requires the file to be owned by the *effective*
46+
> uid the `xrootd` process runs as. A `root`-owned key file will be rejected when
47+
> `xrootd` runs as an unprivileged service account; make the key files owned by
48+
> that account.
49+
50+
`-keys-file` and `-ca-keys-file` are read once at plugin initialization, so a
51+
restart is required to pick up changes. Only the `principal-map-file` is
52+
hot-reloaded (see below).
53+
54+
Validated option ranges:
55+
56+
- `-maxsz <bytes>`: `1`..`524288` (default `8192`)
57+
- `-nonce-ttl <seconds>`: `1`..`600` (default `30`)
58+
59+
### keys-file format
60+
61+
Accepted line formats:
62+
63+
1. Explicit user mapping:
64+
65+
```text
66+
foo ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI...
67+
foo ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQ...
68+
```
69+
70+
2. Authorized-keys style fallback mapping (username extracted from comment prefix before `@`):
71+
72+
```text
73+
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... foo@host
74+
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQ... foo@host
75+
```
76+
77+
### ca-keys-file format (optional)
78+
79+
Each line should contain a CA public key:
80+
81+
```text
82+
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI...
83+
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQ...
84+
```
85+
86+
Also accepted:
87+
88+
```text
89+
cert-authority ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI...
90+
```
91+
92+
When a user certificate is presented, the server validates:
93+
94+
- certificate signature against a trusted CA key
95+
- certificate type is user (`type=1`)
96+
- validity window (`valid_after` / `valid_before`)
97+
- certificate carries no critical options (any critical option is rejected,
98+
i.e. the server fails closed on options such as `force-command` or
99+
`source-address` that it does not enforce)
100+
- principals contain requested user (if principals list is non-empty)
101+
102+
> **Security note:** following OpenSSH semantics, a certificate with an *empty*
103+
> principals list is treated as valid for any requested user. Because the CA is
104+
> fully trusted, only issue zero-principal user certificates if every holder is
105+
> meant to authenticate as an arbitrary account.
106+
107+
### Principal mapping options (cert mode)
108+
109+
Server options:
110+
111+
- `-principal-as-user`:
112+
map a certificate principal directly to local account if it is a valid
113+
local username or uid.
114+
- `-principal-map`:
115+
enable principal mapping file at default path (no argument)
116+
`/etc/xrootd/ssh_principals.map`.
117+
- `-principal-map-file <path>`:
118+
use a custom principal mapping file.
119+
120+
If both direct and file mapping are enabled, direct principal->local-user
121+
mapping is tried first, then the map file.
122+
123+
Map file format:
124+
125+
```text
126+
principal-a alice
127+
principal-b 1001
128+
```
129+
130+
Each line is `<principal> <username|uid>`.
131+
132+
The principal map file is monitored during authentication and is reloaded
133+
automatically if its inode or mtime changes.
134+
135+
## Client configuration
136+
137+
> **Certificate clients:** private key *file* mode only supports raw
138+
> `ssh-ed25519` / `ssh-rsa` keys. To authenticate with an OpenSSH user
139+
> certificate, the client must use `ssh-agent` mode (the certificate identity is
140+
> selected from the agent).
141+
142+
Default mode uses a private key file (PEM/PKCS8 format; supported key types: ed25519, rsa):
143+
144+
```sh
145+
export XRD_SSH_KEY_FILE=/path/to/ed25519-private.pem
146+
```
147+
148+
`XRD_SSH_PRIVATE_KEY_FILE` is accepted as an alias and is consulted when
149+
`XRD_SSH_KEY_FILE` is unset.
150+
151+
Client `ssh-agent` mode is also supported:
152+
153+
```sh
154+
export SSH_AUTH_SOCK=/run/user/1000/ssh-agent.socket
155+
export XRD_SSH_AGENT=1
156+
```
157+
158+
When `XRD_SSH_AGENT=1`, the client picks a supported identity from the agent
159+
and signs the server challenge via agent.
160+
161+
Supported agent identities:
162+
163+
- raw keys: `ssh-ed25519`, `ssh-rsa`
164+
- user certificates: `ssh-ed25519-cert-v01@openssh.com`,
165+
`ssh-rsa-cert-v01@openssh.com`
166+
167+
Optional key selection by fingerprint:
168+
169+
```sh
170+
export XRD_SSH_AGENT_FINGERPRINT='SHA256:base64fingerprint'
171+
```
172+
173+
Fallback behavior:
174+
175+
- if `XRD_SSH_AGENT` is not set, key-file mode is tried first
176+
- if key-file is not configured and `SSH_AUTH_SOCK` exists, agent mode is used
177+
- if not in agent mode, no key-file env is set, and `XRD_SSH_USER` is not set,
178+
the client also tries default key files in order:
179+
- `~/.ssh/id_ed25519`
180+
- `~/.ssh/id_rsa`
181+
- if all methods fail, authentication fails with a detailed error
182+
183+
Optional username override:
184+
185+
```sh
186+
export XRD_SSH_USER=foo
187+
```
188+
189+
If `XRD_SSH_USER` is not set, `USER` is used.
190+
191+
## Notes
192+
193+
- TLS is mandatory (`needTLS() == true`)
194+
- handshake nonce is single-use and expires after `-nonce-ttl`
195+
- debug logging can be enabled via `-debug` or `XrdSecDEBUG=1`; it prints
196+
loaded key metadata (alg/user/fingerprint) and authentication key selection
197+
- RSA signatures (challenge responses and certificate signatures) use
198+
`rsa-sha2-256`. Legacy `ssh-rsa` (SHA-1) signatures are not accepted, so RSA
199+
certificates must be signed by a CA using `rsa-sha2-256`.
200+
201+
## Security considerations
202+
203+
- This protocol authenticates the *client* to the server. It does not
204+
authenticate the server within the handshake and provides no channel binding
205+
between the SSH challenge/response and the underlying TLS session. Its safety
206+
therefore relies on TLS with server-certificate verification to prevent
207+
man-in-the-middle relay of the signed challenge; do not disable TLS peer
208+
verification.
209+
- The challenge is signed over `xrdsec-ssh-v1|<nonce>|<fingerprint>`, binding the
210+
response to the presented key/certificate and the single-use server nonce.
211+
- Only one pending challenge is allowed per transport identity (`tident`); a
212+
second init on the same connection is rejected with `EBUSY`.
213+
- SSH wire string fields are capped at 64 KiB; `keys-file` lines and base64 key
214+
material have separate limits to reduce parser DoS risk at init.
215+
- Client private key files must be owned by the effective uid and must not be
216+
group/other accessible; `ssh-agent` sockets must be owned by the effective uid
217+
and must not be group/other accessible.
218+
- Mapped usernames are restricted to a conservative charset and length before
219+
being stored in `XrdSecEntity.name`.
220+
- Debug logging (`-debug` / `XrdSecDEBUG=1`) prints redacted key fingerprints
221+
and omits transport usernames and socket paths.
222+
- The principal map file is reloaded under a mutex so lookups are not racy with
223+
hot reload.
224+
- In certificate mode the CA is fully trusted: any principal it issues is
225+
accepted (subject to the validity window, `type=1`, and the no-critical-options
226+
rule). Restrict the CA key set in `ca-keys-file` accordingly.

0 commit comments

Comments
 (0)