Skip to content

Commit af65c80

Browse files
committed
httpd: allow passing multiple varlink-httpd.ssh.authorized-keys.*
Allow multiple credentials `varlink-httpd.ssh.authorized-keys.*` to get imported. This can be useful if e.g. one comes via imds and one via a ESP credential.
1 parent d063eb9 commit af65c80

5 files changed

Lines changed: 88 additions & 4 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ async-stream = "0.3"
2828
axum = { version = "0.8.8", features = ["ws"] }
2929
futures-core = "0.3"
3030
serde_json = "1.0.149"
31-
tokio = { version = "1.49.0", features = ["full"] }
31+
tokio = { version = "1.49.0", features = ["rt-multi-thread", "macros", "net", "io-util", "sync", "time", "process", "fs", "signal"] }
3232
# not using default features as we don't need service/server/introspection
3333
zlink = { version = "0.7", default-features = false, features = ["tokio", "proxy", "idl-parse", "tracing"] }
3434
serde = { version = "1", features = ["derive"] }

README.md

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -332,11 +332,22 @@ Ed25519 and ECDSA keys.
332332
#### Server setup
333333

334334
The bridge discovers authorized keys automatically from these
335-
locations (first match wins):
335+
locations:
336336

337-
1. `--authorized-keys PATH` — explicit CLI flag
337+
1. `--authorized-keys PATH` — explicit CLI flag; when given it is the only
338+
source used
338339
2. `/etc/varlink-httpd/authorized_keys` — config file
339-
3. `$CREDENTIALS_DIRECTORY/ssh.authorized_keys.root` — systemd credential (see `systemd.exec(5)`)
340+
3. `$CREDENTIALS_DIRECTORY/ssh.authorized_keys.root` and
341+
`ssh.ephemeral-authorized_keys-all` — systemd credentials (see
342+
`systemd.exec(5)`)
343+
4. `$CREDENTIALS_DIRECTORY/varlink-httpd.ssh.authorized-keys.*` — additional
344+
credentials, imported by the unit using a glob. Each provider (a
345+
generated node config, a local admin, a confext) ships its own
346+
`/etc/credstore/varlink-httpd.ssh.authorized-keys.<provider>` rather than
347+
overwriting a shared file, which neither the credstore nor overlaid
348+
confexts can merge.
349+
350+
Keys from 2, 3 and 4 are merged and deduplicated.
340351

341352
The simplest setup is to pass the path explicitly:
342353

data/varlink-httpd.service.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ ImportCredential=varlink-httpd.tls.trust:trust
5151
# SSH authorized keys (see systemd.system-credentials(7))
5252
ImportCredential=ssh.authorized_keys.root
5353
ImportCredential=ssh.ephemeral-authorized_keys-all
54+
ImportCredential=varlink-httpd.ssh.authorized-keys.*
5455

5556
[Install]
5657
WantedBy=network.target

src/bin/varlink-httpd/auth_ssh.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,30 @@ const SSH_AUTHORIZED_KEYS_CREDENTIALS: &[&str] = &[
308308
"ssh.ephemeral-authorized_keys-all",
309309
];
310310

311+
/// One credential per provider, because neither the credstore nor overlaid
312+
/// confexts can merge the contents of a shared file.
313+
const SSH_AUTHORIZED_KEYS_PREFIX: &str = "varlink-httpd.ssh.authorized-keys.";
314+
315+
/// Sorted so the merge order is stable. Enumerated rather than watched like
316+
/// the names above: systemd fills `$CREDENTIALS_DIRECTORY` at unit start and
317+
/// it is read-only after, so no new match can appear while we run.
318+
fn ssh_authorized_keys_prefixed(dir: &std::path::Path) -> Vec<String> {
319+
let Ok(entries) = std::fs::read_dir(dir) else {
320+
return Vec::new();
321+
};
322+
let mut paths: Vec<String> = entries
323+
.flatten()
324+
.filter(|e| {
325+
e.file_name()
326+
.to_str()
327+
.is_some_and(|n| n.starts_with(SSH_AUTHORIZED_KEYS_PREFIX))
328+
})
329+
.map(|e| e.path().to_string_lossy().into_owned())
330+
.collect();
331+
paths.sort();
332+
paths
333+
}
334+
311335
pub(crate) fn create_ssh_authenticator(
312336
cli_authorized_keys: Option<String>,
313337
creds_dir: Option<&std::path::Path>,
@@ -329,6 +353,7 @@ pub(crate) fn create_ssh_authenticator(
329353
for name in SSH_AUTHORIZED_KEYS_CREDENTIALS {
330354
paths.push(d.join(name).to_string_lossy().to_string());
331355
}
356+
paths.extend(ssh_authorized_keys_prefixed(d));
332357
}
333358
paths
334359
};

src/bin/varlink-httpd/tests.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2017,6 +2017,53 @@ mod sshauth_tests {
20172017
);
20182018
}
20192019

2020+
#[test]
2021+
fn test_ssh_auth_multiple_authorized_keys_credentials() {
2022+
let keygen_dir_a = tempfile::tempdir().unwrap();
2023+
let (pubkey_a, _) = generate_ed25519_keypair(keygen_dir_a.path());
2024+
let keygen_dir_b1 = tempfile::tempdir().unwrap();
2025+
let (pubkey_b1, _) = generate_ed25519_keypair(keygen_dir_b1.path());
2026+
let keygen_dir_b2 = tempfile::tempdir().unwrap();
2027+
let (pubkey_b2, _) = generate_ed25519_keypair(keygen_dir_b2.path());
2028+
let empty_root = tempfile::tempdir().unwrap();
2029+
2030+
// Credentials and the well-known names are merged into a list.
2031+
// The order is stable.
2032+
let creds_dir_multi = tempfile::tempdir().unwrap();
2033+
std::fs::write(
2034+
creds_dir_multi.path().join("ssh.authorized_keys.root"),
2035+
pubkey_a.as_bytes(),
2036+
)
2037+
.unwrap();
2038+
std::fs::write(
2039+
creds_dir_multi
2040+
.path()
2041+
.join("varlink-httpd.ssh.authorized-keys.nodeconfig"),
2042+
format!("{}\n", pubkey_b1.trim()),
2043+
)
2044+
.unwrap();
2045+
std::fs::write(
2046+
creds_dir_multi
2047+
.path()
2048+
.join("varlink-httpd.ssh.authorized-keys.local"),
2049+
format!("{}\n", pubkey_b2.trim()),
2050+
)
2051+
.unwrap();
2052+
// An unrelated credential in the same directory must not be read.
2053+
std::fs::write(
2054+
creds_dir_multi.path().join("varlink-httpd.tls.certificate"),
2055+
b"not a key",
2056+
)
2057+
.unwrap();
2058+
let auth = create_ssh_authenticator(None, Some(creds_dir_multi.path()), empty_root.path())
2059+
.unwrap();
2060+
assert_eq!(
2061+
auth.key_count(),
2062+
3,
2063+
".root (1 key) + two per-provider credentials (1 key each) should be merged"
2064+
);
2065+
}
2066+
20202067
#[test_with::path(/usr/bin/varlinkctl)]
20212068
#[test_with::path(/run/systemd/io.systemd.Hostname)]
20222069
#[tokio::test]

0 commit comments

Comments
 (0)