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...");
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);
let mut results = arc_self.results.lock().await;
@ -537,7 +537,7 @@ impl Deployment {
if self.options.upload_keys && !pre_activation_keys.is_empty() {
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);
let mut results = self.results.lock().await;
@ -561,7 +561,7 @@ impl Deployment {
if self.options.upload_keys && !post_activation_keys.is_empty() {
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);
let mut results = self.results.lock().await;

View File

@ -348,10 +348,56 @@ let
lib.optional (length remainingKeys != 0)
"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 {
modules = [
assertionModule
nixpkgsModule
keyChownModule
deploymentOptions
hive.defaults
config

View File

@ -18,12 +18,13 @@ use crate::util::capture_stream;
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()
.replace("%DESTINATION%", destination.to_str().unwrap())
.replace("%USER%", &escape(key.user().into()))
.replace("%GROUP%", &escape(key.group().into()))
.replace("%PERMISSIONS%", &escape(key.permissions().into()))
.replace("%REQUIRE_OWNERSHIP%", if require_ownership { "1" } else { "" })
.trim_end_matches('\n').to_string();
escape(key_script.into())

View File

@ -5,11 +5,12 @@ tmp="${destination}.tmp"
user=%USER%
group=%GROUP%
permissions=%PERMISSIONS%
require_ownership=%REQUIRE_OWNERSHIP%
mkdir -p $(dirname "$destination")
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"
else
>&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),
}
}
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 {
self.upload_key(&name, &key).await?;
self.upload_key(&name, &key, require_ownership).await?;
}
Ok(())
@ -109,11 +109,11 @@ impl Host for Local {
impl Local {
/// "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));
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");

View File

@ -96,9 +96,13 @@ pub trait Host: Send + Sync + std::fmt::Debug {
Ok(())
}
#[allow(unused_variables)]
/// 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)
}

View File

@ -53,9 +53,9 @@ impl Host for Ssh {
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 {
self.upload_key(&name, &key).await?;
self.upload_key(&name, &key, require_ownership).await?;
}
Ok(())
@ -227,11 +227,11 @@ impl Ssh {
}
/// 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));
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]);