From 7fb4d37e19469b1bf567dd57cb86ae9f9f9d44c0 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Wed, 6 Apr 2022 20:01:41 +0300 Subject: feat: add a user_ca_pubkey field to config, spec With this, there's no need to install an SSH key into root's authorized_keys file. Sponsored-by: author --- src/cloudinit.rs | 25 ++++++++++++++++++++++++- src/config.rs | 4 ++++ src/spec.rs | 51 ++++++++++++++++++++++++++++++++++++++++----------- vmadm.md | 11 ++++++++++- 4 files changed, 78 insertions(+), 13 deletions(-) diff --git a/src/cloudinit.rs b/src/cloudinit.rs index 4fdf9d1..f5db9bf 100644 --- a/src/cloudinit.rs +++ b/src/cloudinit.rs @@ -44,7 +44,7 @@ log(f"loading user-data from {filename}") obj = yaml.safe_load(open(filename)) ssh_keys = obj.get("ssh_keys", {}) -# log(f"ssh_keys: {json.dumps(ssh_keys)}") +user_ca_pubkey = obj.get("user_ca_pubkey", {}) keys = [] certs = [] @@ -77,6 +77,11 @@ for key_type in key_types: with open(filename, "w") as f: f.write(cert) +user_ca_filename = os.path.join(etc, "user-ca-keys") +if user_ca_pubkey: + with open(user_ca_filename, "w") as f: + f.write(user_ca_pubkey) + config = os.path.join(etc, "sshd_config") data = "" if os.path.exists(config): @@ -93,6 +98,9 @@ with open(config, "w") as f: for filename in certs: log(f"hostcert {filename}") f.write(f"hostcertificate {filename}\n") + if user_ca_pubkey: + log(f"trustedusercakeys {user_ca_filename}") + f.write(f"trustedusercakeys {user_ca_filename}\n") f.write(data) log("vmadm cloud-init script ending") @@ -118,6 +126,10 @@ pub enum CloudInitError { #[error(transparent)] KeyError(#[from] KeyError), + /// Something went wrong reading a file. + #[error("could not read {0}: {1}")] + ReadError(PathBuf, #[source] std::io::Error), + /// Something went wrong doing I/O. #[error("could not write to {0}: {1}")] WriteError(PathBuf, #[source] std::io::Error), @@ -172,14 +184,25 @@ struct Userdata { #[serde(skip_serializing_if = "Option::is_none")] ssh_keys: Option, + #[serde(skip_serializing_if = "Option::is_none")] + user_ca_pubkey: Option, + runcmd: Vec, } impl Userdata { fn from(spec: &Specification) -> Result { + let user_ca_pubkey = if let Some(filename) = &spec.user_ca_pubkey { + let data = std::fs::read(&filename) + .map_err(|err| CloudInitError::ReadError(filename.to_path_buf(), err))?; + Some(String::from_utf8(data)?) + } else { + None + }; Ok(Self { ssh_authorized_keys: spec.ssh_keys.clone(), ssh_keys: Hostkeys::from(spec)?, + user_ca_pubkey, runcmd: vec![ format!("python3 -c {}", quote(SCRIPT)), "systemctl reload ssh".to_string(), diff --git a/src/config.rs b/src/config.rs index 034150b..37cd98e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -41,6 +41,9 @@ pub struct Configuration { /// Path name to SSH CA key for creating SSH host certificates. pub ca_key: Option, + + /// Path name to SSH CA public key for verifying SSH user certificates. + pub user_ca_pubkey: Option, } /// Errors from this module. @@ -94,6 +97,7 @@ impl Configuration { expand_optional_pathbuf(&mut self.image_directory)?; expand_optional_pathbuf(&mut self.image_directory)?; expand_optional_pathbuf(&mut self.ca_key)?; + expand_optional_pathbuf(&mut self.user_ca_pubkey)?; expand_optional_pathbufs(&mut self.authorized_keys)?; Ok(()) } diff --git a/src/spec.rs b/src/spec.rs index 150f404..58d7550 100644 --- a/src/spec.rs +++ b/src/spec.rs @@ -33,19 +33,32 @@ struct OneVmInputSpecification { pub autostart: Option, pub networks: Option>, pub ca_key: Option, + pub user_ca_pubkey: Option, } impl OneVmInputSpecification { - fn ssh_key_files( - &self, - config: &Configuration, - name: &str, - ) -> Result, SpecificationError> { - get( + fn ssh_key_files(&self, config: &Configuration, name: &str) -> Option> { + if let Ok(x) = get( &self.ssh_key_files, &config.authorized_keys, SpecificationError::NoAuthorizedKeys(name.to_string()), - ) + ) { + Some(x) + } else { + None + } + } + + fn user_ca_pubkey(&self, config: &Configuration, name: &str) -> Option { + if let Ok(x) = get( + &self.user_ca_pubkey, + &config.user_ca_pubkey, + SpecificationError::NoAuthorizedKeys(name.to_string()), + ) { + Some(x) + } else { + None + } } fn base_image( @@ -199,6 +212,9 @@ pub struct Specification { /// Path to CA key for creating host certificate. pub ca_key: Option, + /// Path to CA publicv key for verifying user certificates. + pub user_ca_pubkey: Option, + /// List of networks to which host should be added. pub networks: Vec, } @@ -226,8 +242,8 @@ pub enum SpecificationError { #[error("No CPU count specified for {0} and no default configured")] NoCpuCount(String), - /// No SSH authorized keys specified. - #[error("No SSH authorized keys specified for {0} and no default configured")] + /// No SSH authorized keys or user CA specified. + #[error("No SSH authorized keys nor user CA specified for {0} and no default configured")] NoAuthorizedKeys(String), /// Error reading specification file. @@ -289,8 +305,20 @@ impl Specification { name: &str, input: &OneVmInputSpecification, ) -> Result { - let key_filenames = input.ssh_key_files(config, name)?; - let ssh_keys = ssh_keys(&key_filenames)?; + let ssh_keys = if let Some(key_filenames) = input.ssh_key_files(config, name) { + ssh_keys(&key_filenames)? + } else { + vec![] + }; + let user_ca_pubkey = input.user_ca_pubkey(config, name); + if ssh_keys.is_empty() && user_ca_pubkey.is_none() { + return Err(SpecificationError::NoAuthorizedKeys(name.to_string())); + } + let user_ca_pubkey = if let Some(filename) = user_ca_pubkey { + Some(expand_tilde(&filename)?) + } else { + None + }; let ca_key = if let Some(filename) = &input.ca_key { Some(expand_tilde(filename)?) } else { @@ -326,6 +354,7 @@ impl Specification { generate_host_certificate: gen_cert, autostart: input.autostart(config), ca_key, + user_ca_pubkey, networks, }; diff --git a/vmadm.md b/vmadm.md index 71a49d1..9c6d3aa 100644 --- a/vmadm.md +++ b/vmadm.md @@ -71,6 +71,7 @@ default_cpus: 1 default_generate_host_certificate: true default_autostart: true ca_key: ~/ca_key +user_ca_pubkey: ~/user_ca_pubkey authorized_keys: - ~/.ssh/id_rsa.pub ~~~ @@ -88,6 +89,7 @@ authorized_keys: "network=default" ], "ca_key": "~/ca_key", + "user_ca_pubkey": "~/user_ca_pubkey", "authorized_keys": [ "~/.ssh/id_rsa.pub" ] @@ -122,7 +124,8 @@ foo: "cpus": 1, "generate_host_certificate": true, "autostart": true, - "ca_key": "~/other_ca" + "ca_key": "~/other_ca", + "user_ca_pubkey": "~/user_ca_pubkey" } ] ~~~ @@ -164,6 +167,7 @@ given an installed vmadm given file init.yaml given file config.yaml given file .ssh/id_rsa.pub from init_ssh_key_pub +given file user_ca_pubkey from ssh_key_pub given file expected/init-test/meta-data from init-metadata given file expected/init-test/user-data from init-userdata when I run vmadm cloud-init --config config.yaml init.yaml actual @@ -211,6 +215,8 @@ ssh_keys: ecdsa_certificate: ecdsa-certificate ed25519_private: ed25519-private ed25519_certificate: ed25519-certificate +user_ca_pubkey: > + ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQChZ6mVuGLBpW7SarFU/Tu6TemquNxatbMUZuTk8RqVtbkvTKeWFZ5h5tntWPHgST8ykYFaIrr8eYuKQkKdBxHW7H8kejTNwRu/rDbRYX5wxTn4jw4RVopGTpxMlGrWeu5CkWPoLAhQtIzzUAnrDGp9sqG6P1G4ohI61wZMFQta9R2uNxXnnes+e2r4Y78GxmlQH/o0ouI8fBnsxRK0IoSfFs2LutO6wjyzR59FdC9TT7wufd5kXMRzxsmPGeXzNcaqvHGxBvRucGFclCkqSRwk3GNEpXZQhlCIoTIoRu0IPAp/430tlx9zJMhhwDlZsOOXRrFYpdWVMSTAAKECLSYx liw@exolobe1 ~~~ # Create a virtual machine @@ -228,6 +234,7 @@ given a Debian 10 OpenStack cloud image given file smoke.yaml given file config.yaml given file ca_key +given file user_ca_pubkey from ssh_key_pub given file .ssh/id_rsa from ssh_key given file .ssh/id_rsa.pub from ssh_key_pub given file .ssh/config from ssh_config @@ -290,6 +297,7 @@ given file smoke.yaml given file other.yaml given file config.yaml given file ca_key +given file user_ca_pubkey from ssh_key_pub given file .ssh/id_rsa from ssh_key given file .ssh/id_rsa.pub from ssh_key_pub given file .ssh/config from ssh_config @@ -351,6 +359,7 @@ given a Debian 10 OpenStack cloud image given file smoke.yaml given file config.yaml given file ca_key +given file user_ca_pubkey from ssh_key_pub given file .ssh/id_rsa from ssh_key given file .ssh/id_rsa.pub from ssh_key_pub given file .ssh/config from ssh_config -- cgit v1.2.1