Commit

Author:

Hash:

Timestamp:

+172 -214 +/-13 browse

Kevin Schoon [me@kevinschoon.com]

2c5281f425eda6502bbd3a6cf71644d8e29c4e70

Fri, 25 Jul 2025 21:19:27 +0000 (7 months ago)

simplify ayllu-keys considerably, add new identity create
1diff --git a/Cargo.lock b/Cargo.lock
2index b2dd797..de317ae 100644
3--- a/Cargo.lock
4+++ b/Cargo.lock
5 @@ -372,12 +372,9 @@ name = "ayllu-keys"
6 version = "0.1.0"
7 dependencies = [
8 "ayllu_config",
9- "ayllu_logging",
10+ "ayllu_identity",
11 "clap 4.5.41",
12 "serde",
13- "thiserror 2.0.12",
14- "tracing",
15- "tracing-subscriber",
16 ]
17
18 [[package]]
19 @@ -429,6 +426,14 @@ dependencies = [
20 ]
21
22 [[package]]
23+ name = "ayllu_identity"
24+ version = "0.1.0"
25+ dependencies = [
26+ "openssh-keys",
27+ "serde",
28+ ]
29+
30+ [[package]]
31 name = "ayllu_logging"
32 version = "0.1.0"
33 dependencies = [
34 diff --git a/Cargo.toml b/Cargo.toml
35index 7ae401c..e428cbe 100644
36--- a/Cargo.toml
37+++ b/Cargo.toml
38 @@ -5,7 +5,8 @@ members = [
39 "crates/config",
40 "crates/git",
41 "crates/logging",
42- "crates/timeutil",
43+ "crates/timeutil",
44+ "crates/identity",
45 # "crates/scheduler",
46 # "crates/database",
47 "ayllu",
48 @@ -15,7 +16,7 @@ members = [
49 "ayllu-keys",
50 # "ayllu-jobs",
51 # "ayllu-xmpp",
52- "quipu",
53+ "quipu", "crates/identity",
54 ]
55
56 [workspace.dependencies]
57 @@ -33,6 +34,7 @@ futures = "0.3.31"
58 futures-util = "0.3.31"
59 sqlx = { version = "0.8.6", features = [ "runtime-tokio-rustls", "sqlite", "macros", "time" ] }
60 tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
61+ openssh-keys = "0.6.4"
62
63 tokio = { version = "1.46.1", features = ["full"] }
64 tokio-util = { version = "0.7.15", features = ["io", "compat"] }
65 diff --git a/ayllu-keys/Cargo.toml b/ayllu-keys/Cargo.toml
66index 4d7ddbd..b35b9f2 100644
67--- a/ayllu-keys/Cargo.toml
68+++ b/ayllu-keys/Cargo.toml
69 @@ -6,10 +6,7 @@ edition = "2021"
70 [dependencies]
71
72 ayllu_config = { path = "../crates/config" }
73- ayllu_logging = { path = "../crates/logging" }
74+ ayllu_identity = { path = "../crates/identity" }
75
76 clap = { workspace = true }
77 serde = { workspace = true }
78- tracing = { workspace = true }
79- tracing-subscriber = { workspace = true }
80- thiserror = {workspace = true}
81 diff --git a/ayllu-keys/src/config.rs b/ayllu-keys/src/config.rs
82deleted file mode 100644
83index a466687..0000000
84--- a/ayllu-keys/src/config.rs
85+++ /dev/null
86 @@ -1,23 +0,0 @@
87- use std::path::PathBuf;
88-
89- use ayllu_config::Configurable;
90- use serde::{Deserialize, Serialize};
91-
92- /// An identity defines a known user in the Ayllu environment
93- #[derive(Serialize, Deserialize, Clone)]
94- pub struct Identity {
95- pub username: String,
96- pub shell: Option<PathBuf>,
97- pub authorized_keys: Option<Vec<String>>,
98- }
99-
100- #[derive(Serialize, Deserialize, Clone, Default)]
101- pub struct Config {
102- pub log_level: String,
103- // path to the ayllu-shell executable
104- pub ayllu_shell: Option<PathBuf>,
105- #[serde(default = "Vec::new")]
106- pub identities: Vec<Identity>,
107- }
108-
109- impl Configurable for Config {}
110 diff --git a/ayllu-keys/src/keys.rs b/ayllu-keys/src/keys.rs
111deleted file mode 100644
112index b0721fa..0000000
113--- a/ayllu-keys/src/keys.rs
114+++ /dev/null
115 @@ -1,63 +0,0 @@
116- #[derive(thiserror::Error, Debug)]
117- pub enum Error {
118- #[error("Missing Key Type")]
119- MissingKeyType,
120- #[error("Missing Key Body")]
121- MissingKeyBody,
122- }
123-
124- #[derive(Clone, Debug)]
125- pub struct Entry {
126- key_type: String,
127- key_body: String,
128- }
129-
130- #[derive(Clone, Debug)]
131- pub struct Entries(Vec<Entry>);
132-
133- impl TryFrom<String> for Entry {
134- type Error = Error;
135-
136- fn try_from(value: String) -> Result<Self, Self::Error> {
137- let mut split = value.splitn(3, " ");
138- let key_type = split
139- .next()
140- .map(|kt| Ok(kt.to_string()))
141- .unwrap_or(Err(Error::MissingKeyType))?;
142- let key_body = split
143- .next()
144- .map(|kb| Ok(kb.to_string()))
145- .unwrap_or(Err(Error::MissingKeyBody))?;
146- Ok(Entry { key_type, key_body })
147- }
148- }
149-
150- impl TryFrom<&Vec<String>> for Entries {
151- type Error = Error;
152-
153- fn try_from(value: &Vec<String>) -> Result<Self, Self::Error> {
154- Ok(Entries(value.iter().try_fold(
155- Vec::new(),
156- |mut accm, entry| {
157- let entry = Entry::try_from(entry.clone())?;
158- accm.push(entry);
159- Ok(accm)
160- },
161- )?))
162- }
163- }
164-
165- impl Entries {
166- pub fn find(&self, key_type: &str, key_body: &str) -> Option<Entry> {
167- self.0
168- .iter()
169- .filter_map(|entry| {
170- if key_type == entry.key_type && key_body == entry.key_body {
171- Some(entry.clone())
172- } else {
173- None
174- }
175- })
176- .next()
177- }
178- }
179 diff --git a/ayllu-keys/src/main.rs b/ayllu-keys/src/main.rs
180index b9439f9..914bdf4 100644
181--- a/ayllu-keys/src/main.rs
182+++ b/ayllu-keys/src/main.rs
183 @@ -5,44 +5,74 @@
184 /// requesting shell access via a regular ssh channel.
185 /// * An Ayllu managed user is requesting shell access.
186 /// * An Ayllu managed user is running git operations as the git service.
187- use std::path::{Path, PathBuf};
188+ use std::{
189+ path::{Path, PathBuf},
190+ process::ExitCode,
191+ };
192
193+ use ayllu_config::Configurable;
194+ use ayllu_identity::Identity;
195 use clap::Parser;
196
197- mod config;
198- mod keys;
199+ use serde::{Deserialize, Serialize};
200
201- use tracing::Level;
202+ const GLOBAL_AYLLU_CONFIG: &str = "/etc/ayllu/config.toml";
203+ const DEFAULT_AYLLU_SHELL_PATH: &str = "/usr/bin/ayllu-shell";
204
205- /// User git is a special case that allows users without shell access to
206- /// login over ssh with the git user.
207- const GIT_USER: &str = "git";
208+ #[derive(Serialize, Deserialize, Clone, Default)]
209+ pub struct Config {
210+ // path to the ayllu-shell executable
211+ pub ayllu_shell: Option<PathBuf>,
212+ #[serde(default = "Vec::new")]
213+ pub identities: Vec<Identity>,
214+ }
215
216- const DEFAULT_AYLLU_SHELL_PATH: &str = "/usr/bin/ayllu-shell";
217+ impl Configurable for Config {}
218
219 #[derive(Parser, Debug)]
220 #[clap(version, about, long_about = "Ayllu Keys")]
221 struct Arguments {
222 username: String,
223- home_dir: String,
224+ home_dir: PathBuf,
225 ca_key_type: String,
226 certificate: String,
227 #[clap(short, long, value_name = "FILE")]
228 config: Option<PathBuf>,
229- #[clap(short, long, value_name = "log_path")]
230- log_path: Option<PathBuf>,
231 #[clap(long, value_name = "ayllu-shell")]
232 ayllu_shell: Option<PathBuf>,
233 }
234
235- fn main() -> Result<(), Box<dyn std::error::Error>> {
236- let args = Arguments::parse();
237- if let Some(log_path) = args.log_path {
238- ayllu_logging::init_file(Level::INFO, log_path.as_path())
239+ fn authorized_keys(home_dir: &Path) -> ExitCode {
240+ let authorized_keys_path = home_dir.join(".ssh/authorized_keys");
241+ if let Ok(mut fp) = std::fs::File::open(authorized_keys_path) {
242+ match std::io::copy(&mut fp, &mut std::io::stdout()) {
243+ Ok(_) => ExitCode::SUCCESS,
244+ Err(e) => {
245+ // results in access denied
246+ eprintln!("Failed to read authorized keys file: {}", e);
247+ ExitCode::FAILURE
248+ }
249+ }
250 } else {
251- ayllu_logging::init(Level::DEBUG)
252+ ExitCode::FAILURE
253+ }
254+ }
255+
256+ fn main() -> ExitCode {
257+ let args = Arguments::parse();
258+
259+ if args.username != "ayllu" {
260+ return authorized_keys(&args.home_dir);
261+ }
262+
263+ let config: Config = match ayllu_config::Reader::load(args.config.as_deref()) {
264+ Ok(cfg) => cfg,
265+ Err(e) => {
266+ eprintln!("Cannot read configuration: {e:?}");
267+ return ExitCode::FAILURE;
268+ }
269 };
270- let config: config::Config = ayllu_config::Reader::load(args.config.as_deref())?;
271+
272 let ayllu_shell_path = args
273 .ayllu_shell
274 .as_ref()
275 @@ -50,88 +80,42 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
276 pb.to_string_lossy().to_string()
277 });
278
279- tracing::info!(
280- "Login attempt for user: {} home={}, key_type={}, certificate={}",
281- args.username,
282- args.home_dir,
283- args.ca_key_type,
284- args.certificate
285+ eprintln!(
286+ "Login attempt for user: {} home={:?}, key_type={}, certificate={}",
287+ args.username, args.home_dir, args.ca_key_type, args.certificate
288 );
289
290- let authenticating_as_git = args.username == GIT_USER;
291-
292- let identity = config
293- .identities
294- .iter()
295- .find_map(|identity| {
296- if let Some(authorized_keys) = identity.authorized_keys.as_ref() {
297- match crate::keys::Entries::try_from(authorized_keys) {
298- Ok(entries) => {
299- if entries.find(&args.ca_key_type, &args.certificate).is_some() {
300- Some(Ok(identity.clone()))
301- } else {
302- None
303- }
304- }
305- Err(err) => Some(Err(err)),
306- }
307- } else {
308- None
309- }
310- })
311- .transpose()?;
312-
313- if let Some(identity) = identity {
314- let shell = identity.shell.as_ref();
315- if !authenticating_as_git {
316- // if not authenticating as the git user we have to verify the
317- // user is who they say they are by comparing their username to
318- // what their identity is configured as.
319- if args.username != identity.username {
320- tracing::warn!(
321- "User {} attempted to impersonate {}",
322- identity.username,
323- args.username
324- );
325- return Ok(());
326- }
327+ let user_key = match ayllu_identity::PublicKey::parse(&format!(
328+ "{} {}",
329+ args.ca_key_type, args.certificate
330+ )) {
331+ Ok(key) => key,
332+ Err(e) => {
333+ eprintln!("Cannot parse SSH key from sshd: {e:?}");
334+ return ExitCode::FAILURE;
335 }
336- if let Some(authorized_keys) = &identity.authorized_keys {
337- authorized_keys.iter().for_each(|public_key| {
338- let mut preamble = String::from("restrict");
339- if shell.is_some() {
340- // If the global configuration permits shell access we
341- // allow for the allocation of a pty, otherwise this is
342- // disabled.
343- preamble.push_str(",pty");
344- }
345- preamble.push_str(&format!(",command=\"{}\"", ayllu_shell_path));
346- preamble.push_str(&format!(
347- ",environment=\"AYLLU_USERNAME={}\"",
348- identity.username
349- ));
350- preamble.push_str(&format!(" {}", public_key));
351- tracing::info!("{}", preamble);
352- println!("{}", preamble);
353- })
354- };
355- } else {
356- tracing::info!(
357- "No Ayllu managed user {} found, falling back to OS",
358- args.username
359- );
360- // fallback to ~/.ssh/authorized_keys if identity is not known
361- let authorized_keys_path = Path::new(&args.home_dir).join(".ssh/authorized_keys");
362- if let Ok(mut fp) = std::fs::File::open(authorized_keys_path) {
363- match std::io::copy(&mut fp, &mut std::io::stdout()) {
364- Ok(_) => {}
365- Err(e) => {
366- // results in access denied
367- tracing::error!("Failed to read authorized keys file: {}", e);
368- return Err(Box::new(e));
369- }
370- }
371+ };
372+
373+ let identity = match config.identities.iter().find(|identity| {
374+ identity
375+ .authorized_keys
376+ .iter()
377+ .find(|authorized_key| authorized_key.0 == user_key)
378+ .is_some()
379+ }) {
380+ Some(identity) => identity,
381+ None => {
382+ eprintln!("Cannot identity a valid Ayllu user");
383+ return ExitCode::FAILURE;
384 }
385- }
386- Ok(())
387+ };
388+
389+ identity.authorized_keys.iter().for_each(|authorized_key| {
390+ println!(
391+ "restrict,pty,command=\"{ayllu_shell_path} --config {GLOBAL_AYLLU_CONFIG}\",environment=\"AYLLU_USERNAME={}\" {}",
392+ identity.username, authorized_key.0
393+ );
394+ });
395+
396+ ExitCode::SUCCESS
397 }
398 diff --git a/ayllu-shell/src/main.rs b/ayllu-shell/src/main.rs
399index 2de4240..6a05851 100644
400--- a/ayllu-shell/src/main.rs
401+++ b/ayllu-shell/src/main.rs
402 @@ -9,19 +9,19 @@ mod ui;
403 #[derive(Parser, Debug)]
404 #[clap(version, about, long_about = "Ayllu Shell Access")]
405 struct Arguments {
406- #[clap(short, long, value_name = "FILE")]
407- config: Option<PathBuf>,
408 /// logging level [ERROR,WARN,INFO,DEBUG,TRACE]
409 #[clap(short, long)]
410 level: Option<String>,
411+ #[clap(short, long)]
412+ config: Option<PathBuf>,
413 }
414
415 fn main() -> Result<(), Box<dyn std::error::Error>> {
416 let args = Arguments::parse();
417 let config: config::Config = ayllu_config::Reader::load(args.config.as_deref())?;
418- // This value must already exist and is validated in ayllu-keys, thus no
419- // authentication is required here.
420- let username = std::env::var("USER").unwrap();
421+ // If AYLLU_USERNAME is set then it's authenticated via ayllu-keys and we assume that identity
422+ // otherwise we assume the identity of the user who is calling it.
423+ let username = std::env::var("AYLLU_USERNAME").unwrap_or(std::env::var("USER").unwrap());
424 let identity = config
425 .identities
426 .iter()
427 diff --git a/ayllu-shell/src/ui.rs b/ayllu-shell/src/ui.rs
428index bbc614d..c612791 100644
429--- a/ayllu-shell/src/ui.rs
430+++ b/ayllu-shell/src/ui.rs
431 @@ -165,7 +165,7 @@ mod menu {
432
433 // Main Menu Items
434
435- pub const INITIAL_ITEMS: &[Item] = &[Item::Create, Item::Browse, Item::Shell, Item::Exit];
436+ pub const INITIAL_ITEMS: &[Item] = &[Item::Create, Item::Browse, Item::Exit];
437 pub enum Item<'a> {
438 /// Select a collection
439 Collection(&'a Collection),
440 @@ -194,8 +194,6 @@ mod menu {
441 description: Option<String>,
442 path: PathBuf,
443 },
444- /// Drop into a shell
445- Shell,
446 /// Exit the prompt
447 Exit,
448 }
449 @@ -213,7 +211,6 @@ mod menu {
450 name: _,
451 } => write!(f, "Delete"),
452 Item::Browse => write!(f, "Browse Repositories"),
453- Item::Shell => write!(f, "Drop into a Shell"),
454 Item::Exit => write!(f, "Exit"),
455 Item::Repository {
456 collection,
457 @@ -399,7 +396,6 @@ impl Prompt<'_> {
458 None => self.execute(None, None),
459 }
460 }
461- Some(menu::Item::Shell) => todo!(),
462 Some(menu::Item::Exit) => return Ok(()),
463 Some(menu::Item::Repository {
464 collection,
465 diff --git a/crates/identity/Cargo.toml b/crates/identity/Cargo.toml
466new file mode 100644
467index 0000000..343b20c
468--- /dev/null
469+++ b/crates/identity/Cargo.toml
470 @@ -0,0 +1,8 @@
471+ [package]
472+ name = "ayllu_identity"
473+ version = "0.1.0"
474+ edition = "2024"
475+
476+ [dependencies]
477+ serde = { workspace = true }
478+ openssh-keys = { workspace = true }
479 diff --git a/crates/identity/src/lib.rs b/crates/identity/src/lib.rs
480new file mode 100644
481index 0000000..e8871e3
482--- /dev/null
483+++ b/crates/identity/src/lib.rs
484 @@ -0,0 +1,33 @@
485+ use serde::{Deserialize, Serialize};
486+
487+ pub use openssh_keys::PublicKey;
488+
489+ #[derive(Clone, Debug)]
490+ pub struct WrappedKey(pub PublicKey);
491+
492+ impl Serialize for WrappedKey {
493+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
494+ where
495+ S: serde::Serializer,
496+ {
497+ self.0.to_string().serialize(serializer)
498+ }
499+ }
500+
501+ impl<'de> Deserialize<'de> for WrappedKey {
502+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
503+ where
504+ D: serde::Deserializer<'de>,
505+ {
506+ let key_str = String::deserialize(deserializer)?;
507+ let pub_key = openssh_keys::PublicKey::parse(&key_str).map_err(serde::de::Error::custom)?;
508+ Ok(WrappedKey(pub_key))
509+ }
510+ }
511+
512+ #[derive(Serialize, Deserialize, Clone, Default)]
513+ pub struct Identity {
514+ pub username: String,
515+ #[serde(default = "Vec::new")]
516+ pub authorized_keys: Vec<WrappedKey>,
517+ }
518 diff --git a/packaging/archlinux/ayllu-git/PKGBUILD b/packaging/archlinux/ayllu-git/PKGBUILD
519index a729e1e..f2756ea 100644
520--- a/packaging/archlinux/ayllu-git/PKGBUILD
521+++ b/packaging/archlinux/ayllu-git/PKGBUILD
522 @@ -20,7 +20,7 @@ makedepends=(
523 provides=("ayllu-git")
524 optdepends=()
525 source=(
526- "$_pkgname::git+https://ayllu-forge.org/ayllu/${_pkgname}"
527+ "$_pkgname::git+https://ayllu-forge.org/ayllu/ayllu#branch=main"
528 )
529 # See: https://gitlab.archlinux.org/archlinux/packaging/packages/pacman/-/issues/20
530 options=(!lto)
531 diff --git a/scripts/ayllu_shell_ssh.sh b/scripts/ayllu_shell_ssh.sh
532index fd7ed1c..1e393aa 100755
533--- a/scripts/ayllu_shell_ssh.sh
534+++ b/scripts/ayllu_shell_ssh.sh
535 @@ -1,4 +1,7 @@
536 #!/bin/sh
537- set -e
538+ set -e
539
540- ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no demo@127.0.0.1 -p 2222
541+ # USERNAME="demo"
542+ USERNAME="ayllu"
543+
544+ ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "$USERNAME@127.0.0.1" -p 2222
545 diff --git a/scripts/ayllu_shell_test.sh b/scripts/ayllu_shell_test.sh
546index de5f9eb..f03e69a 100755
547--- a/scripts/ayllu_shell_test.sh
548+++ b/scripts/ayllu_shell_test.sh
549 @@ -1,22 +1,38 @@
550 #!/bin/sh
551 set -e
552 # Run a test server suitable for developing ayllu-shell and related code.
553+ # Your public SSH keys are passed into the container
554 # Expects a recent version of ayllu:multiuser-main on your system
555
556+ # NOTE that the session will terminate after closing the first connection.
557+ DEBUGGING="-d"
558+
559 AYLLU_SRC="$PWD"
560 LOCAL_SSH_PORT="2222"
561- KEYS_COMMAND="/src/target/x86_64-unknown-linux-musl/debug/ayllu-keys --ayllu-shell=/src/target/x86_64-unknown-linux-musl/debug/ayllu-shell --log-path=/tmp/ayllu.log %%u %%h %%t %%k"
562+ KEYS_COMMAND="/src/target/x86_64-unknown-linux-musl/debug/ayllu-keys --ayllu-shell=/src/target/x86_64-unknown-linux-musl/debug/ayllu-shell %u %h %t %k"
563
564 cargo build --target x86_64-unknown-linux-musl --package ayllu-keys
565 cargo build --target x86_64-unknown-linux-musl --package ayllu-shell
566
567+ PUBLIC_KEY="$(find ~/.ssh -name '*.pub' -exec cat {} \; | head -n 1)"
568+
569 init_env() {
570- printf "passwd -d root\n"
571- printf "adduser -h /home/demo -D demo\n"
572- printf "passwd -d demo\n"
573- printf "ssh-keygen -A\n"
574- printf "/usr/sbin/sshd -d -D -o PermitTTY=yes -o PermitUserEnvironment=AYLLU_USERNAME -o PasswordAuthentication=no -o AuthorizedKeysCommand=\"$KEYS_COMMAND\" -o AuthorizedKeysCommandUser=ayllu\n"
575- printf "cat /tmp/ayllu.log\n"
576+ cat<<EOF
577+ passwd -d root
578+ adduser -h /home/demo -D demo
579+ passwd -d demo
580+ adduser -h /home/ayllu -D ayllu
581+ passwd -d ayllu
582+ ssh-keygen -A
583+ mkdir -p /home/demo/.ssh
584+ echo $PUBLIC_KEY > /home/demo/.ssh/authorized_keys
585+ chmod 644 /home/demo/.ssh/authorized_keys
586+ cat /etc/ayllu/config.example.toml > /etc/ayllu/config.toml
587+ echo "[[identities]]" >> /etc/ayllu/config.toml
588+ echo username = \"demo\" >> /etc/ayllu/config.toml
589+ echo authorized_keys = [\"$PUBLIC_KEY\"] >> /etc/ayllu/config.toml
590+ /usr/sbin/sshd $DEBUGGING -D -o PermitTTY=yes -o PermitUserEnvironment=AYLLU_USERNAME -o PasswordAuthentication=no -o AuthorizedKeysCommand="$KEYS_COMMAND" -o AuthorizedKeysCommandUser=root
591+ EOF
592 }
593
594 echo "To open a remote shell:"
595 @@ -25,5 +41,5 @@ echo "Or run scripts/ayllu_shell_ssh.sh"
596
597 podman run \
598 --name ayllu-shell-test \
599- --rm -ti --user root -v $AYLLU_SRC:/src -v $PWD/config.example.toml:/etc/ayllu/config.toml \
600+ --rm -ti --user root -v $AYLLU_SRC:/src -v $PWD/config.example.toml:/etc/ayllu/config.example.toml:ro \
601 -p $LOCAL_SSH_PORT:22 registry.ayllu-forge.org/ayllu/ayllu:multiuser-main sh -c "$(init_env)"