Ensure key ownerships are set correctly

Depending on when keys are uploaded (`deployment.keys.<name>.uploadAt`):

`pre-activation`:
We set the ownerships in the uploader script opportunistically and
continue if the user/group does not exist. Then, in the activation
script, we set the ownerships of all pre-activation keys.

`post-activation`:
We set the ownerships in the uploader script and fail if the
user/group does not exist.

The ownerships will be correct regardless of which mode is in use.

Fixes #23. Also a more complete solution to #10.
This commit is contained in:
Zhaofeng Li 2021-08-26 12:54:41 -07:00
parent e98a66bc2e
commit 7b69946d98
7 changed files with 67 additions and 15 deletions

View file

@ -284,7 +284,7 @@ impl Deployment {
task.log("Uploading keys..."); task.log("Uploading keys...");
if let Err(e) = target.host.upload_keys(&target.config.keys).await { if let Err(e) = target.host.upload_keys(&target.config.keys, true).await {
task.failure_err(&e); task.failure_err(&e);
let mut results = arc_self.results.lock().await; let mut results = arc_self.results.lock().await;
@ -537,7 +537,7 @@ impl Deployment {
if self.options.upload_keys && !pre_activation_keys.is_empty() { if self.options.upload_keys && !pre_activation_keys.is_empty() {
bar.log("Uploading keys..."); bar.log("Uploading keys...");
if let Err(e) = target.host.upload_keys(&pre_activation_keys).await { if let Err(e) = target.host.upload_keys(&pre_activation_keys, false).await {
bar.failure_err(&e); bar.failure_err(&e);
let mut results = self.results.lock().await; let mut results = self.results.lock().await;
@ -561,7 +561,7 @@ impl Deployment {
if self.options.upload_keys && !post_activation_keys.is_empty() { if self.options.upload_keys && !post_activation_keys.is_empty() {
bar.log("Uploading keys (post-activation)..."); bar.log("Uploading keys (post-activation)...");
if let Err(e) = target.host.upload_keys(&post_activation_keys).await { if let Err(e) = target.host.upload_keys(&post_activation_keys, true).await {
bar.failure_err(&e); bar.failure_err(&e);
let mut results = self.results.lock().await; let mut results = self.results.lock().await;

View file

@ -348,10 +348,56 @@ let
lib.optional (length remainingKeys != 0) lib.optional (length remainingKeys != 0)
"The following Nixpkgs configuration keys set in meta.nixpkgs will be ignored: ${toString remainingKeys}"; "The following Nixpkgs configuration keys set in meta.nixpkgs will be ignored: ${toString remainingKeys}";
}; };
# Change the ownership of all keys uploaded pre-activation
#
# This is built as part of the system profile.
# We must be careful not to access `text` / `keyCommand` / `keyFile` here
keyChownModule = { lib, config, ... }: let
preActivationKeys = lib.filterAttrs (name: key: key.uploadAt == "pre-activation") config.deployment.keys;
scriptDeps = if config.system.activationScripts ? groups then [ "groups" ] else [ "users" ];
commands = lib.mapAttrsToList (name: key: let
keyPath = "${key.destDir}/${name}";
in ''
if [ -f "${keyPath}" ]; then
if ! chown ${key.user}:${key.group} "${keyPath}"; then
# Error should be visible in stderr
failed=1
fi
else
>&2 echo "Key ${keyPath} does not exist. Skipping chown."
fi
'') preActivationKeys;
script = lib.stringAfter scriptDeps ''
# This script is injected by Colmena to change the ownerships
# of keys (`deployment.keys`) deployed before system activation.
>&2 echo "setting up key ownerships..."
# We set the ownership of as many keys as possible before failing
failed=
${concatStringsSep "\n" commands}
if [ -n "$failed" ]; then
>&2 echo "Failed to set the ownership of some keys."
# The activation script has a trap to handle failed
# commands and print out various debug information.
# Let's trigger that instead of `exit 1`.
false
fi
'';
in {
system.activationScripts.colmena-chown-keys = lib.mkIf (length commands != 0) script;
};
in evalConfig { in evalConfig {
modules = [ modules = [
assertionModule assertionModule
nixpkgsModule nixpkgsModule
keyChownModule
deploymentOptions deploymentOptions
hive.defaults hive.defaults
config config

View file

@ -18,12 +18,13 @@ use crate::util::capture_stream;
const SCRIPT_TEMPLATE: &'static str = include_str!("./key_uploader.template.sh"); const SCRIPT_TEMPLATE: &'static str = include_str!("./key_uploader.template.sh");
pub fn generate_script<'a>(key: &'a Key, destination: &'a Path) -> Cow<'a, str> { pub fn generate_script<'a>(key: &'a Key, destination: &'a Path, require_ownership: bool) -> Cow<'a, str> {
let key_script = SCRIPT_TEMPLATE.to_string() let key_script = SCRIPT_TEMPLATE.to_string()
.replace("%DESTINATION%", destination.to_str().unwrap()) .replace("%DESTINATION%", destination.to_str().unwrap())
.replace("%USER%", &escape(key.user().into())) .replace("%USER%", &escape(key.user().into()))
.replace("%GROUP%", &escape(key.group().into())) .replace("%GROUP%", &escape(key.group().into()))
.replace("%PERMISSIONS%", &escape(key.permissions().into())) .replace("%PERMISSIONS%", &escape(key.permissions().into()))
.replace("%REQUIRE_OWNERSHIP%", if require_ownership { "1" } else { "" })
.trim_end_matches('\n').to_string(); .trim_end_matches('\n').to_string();
escape(key_script.into()) escape(key_script.into())

View file

@ -5,11 +5,12 @@ tmp="${destination}.tmp"
user=%USER% user=%USER%
group=%GROUP% group=%GROUP%
permissions=%PERMISSIONS% permissions=%PERMISSIONS%
require_ownership=%REQUIRE_OWNERSHIP%
mkdir -p $(dirname "$destination") mkdir -p $(dirname "$destination")
touch "$tmp" touch "$tmp"
if getent passwd "$user" >/dev/null && getent group "$group" >/dev/null; then if [ -n "$require_ownership" ] || getent passwd "$user" >/dev/null && getent group "$group" >/dev/null; then
chown "$user:$group" "$tmp" chown "$user:$group" "$tmp"
else else
>&2 echo "User $user and/or group $group do not exist. Skipping chown." >&2 echo "User $user and/or group $group do not exist. Skipping chown."

View file

@ -61,9 +61,9 @@ impl Host for Local {
Err(e) => Err(e), Err(e) => Err(e),
} }
} }
async fn upload_keys(&mut self, keys: &HashMap<String, Key>) -> NixResult<()> { async fn upload_keys(&mut self, keys: &HashMap<String, Key>, require_ownership: bool) -> NixResult<()> {
for (name, key) in keys { for (name, key) in keys {
self.upload_key(&name, &key).await?; self.upload_key(&name, &key, require_ownership).await?;
} }
Ok(()) Ok(())
@ -109,11 +109,11 @@ impl Host for Local {
impl Local { impl Local {
/// "Uploads" a single key. /// "Uploads" a single key.
async fn upload_key(&mut self, name: &str, key: &Key) -> NixResult<()> { async fn upload_key(&mut self, name: &str, key: &Key, require_ownership: bool) -> NixResult<()> {
self.progress_bar.log(&format!("Deploying key {}", name)); self.progress_bar.log(&format!("Deploying key {}", name));
let dest_path = key.dest_dir().join(name); let dest_path = key.dest_dir().join(name);
let key_script = format!("'{}'", key_uploader::generate_script(key, &dest_path)); let key_script = format!("'{}'", key_uploader::generate_script(key, &dest_path, require_ownership));
let mut command = Command::new("sh"); let mut command = Command::new("sh");

View file

@ -96,9 +96,13 @@ pub trait Host: Send + Sync + std::fmt::Debug {
Ok(()) Ok(())
} }
#[allow(unused_variables)]
/// Uploads a set of keys to the host. /// Uploads a set of keys to the host.
async fn upload_keys(&mut self, keys: &HashMap<String, Key>) -> NixResult<()> { ///
/// If `require_ownership` is false, then the ownership of a key
/// will not be applied if the specified user/group does not
/// exist.
#[allow(unused_variables)]
async fn upload_keys(&mut self, keys: &HashMap<String, Key>, require_ownership: bool) -> NixResult<()> {
Err(NixError::Unsupported) Err(NixError::Unsupported)
} }

View file

@ -53,9 +53,9 @@ impl Host for Ssh {
Err(e) => Err(e), Err(e) => Err(e),
} }
} }
async fn upload_keys(&mut self, keys: &HashMap<String, Key>) -> NixResult<()> { async fn upload_keys(&mut self, keys: &HashMap<String, Key>, require_ownership: bool) -> NixResult<()> {
for (name, key) in keys { for (name, key) in keys {
self.upload_key(&name, &key).await?; self.upload_key(&name, &key, require_ownership).await?;
} }
Ok(()) Ok(())
@ -227,11 +227,11 @@ impl Ssh {
} }
/// Uploads a single key. /// Uploads a single key.
async fn upload_key(&mut self, name: &str, key: &Key) -> NixResult<()> { async fn upload_key(&mut self, name: &str, key: &Key, require_ownership: bool) -> NixResult<()> {
self.progress_bar.log(&format!("Deploying key {}", name)); self.progress_bar.log(&format!("Deploying key {}", name));
let dest_path = key.dest_dir().join(name); let dest_path = key.dest_dir().join(name);
let key_script = key_uploader::generate_script(key, &dest_path); let key_script = key_uploader::generate_script(key, &dest_path, require_ownership);
let mut command = self.ssh(&["sh", "-c", &key_script]); let mut command = self.ssh(&["sh", "-c", &key_script]);