Set NIX_SSL_CERT_FILE in the daemon (#347)

* Set NIX_SSL_CERT_FILE in the daemon

* Fixups
This commit is contained in:
Ana Hobden 2023-03-20 09:38:15 -07:00 committed by GitHub
parent ee06c3c750
commit 371f94ba51
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 168 additions and 119 deletions

View file

@ -1,10 +1,13 @@
use std::path::{Path, PathBuf};
use std::path::PathBuf;
use bytes::{Buf, Bytes};
use reqwest::{Certificate, Url};
use reqwest::Url;
use tracing::{span, Span};
use crate::action::{Action, ActionDescription, ActionError, ActionTag, StatefulAction};
use crate::{
action::{Action, ActionDescription, ActionError, ActionTag, StatefulAction},
parse_ssl_cert,
};
/**
Fetch a URL to the given path
@ -163,23 +166,6 @@ impl Action for FetchAndUnpackNix {
}
}
async fn parse_ssl_cert(ssl_cert_file: &Path) -> Result<Certificate, ActionError> {
let cert_buf = tokio::fs::read(ssl_cert_file)
.await
.map_err(|e| ActionError::Read(ssl_cert_file.to_path_buf(), e))?;
// We actually try them since things could be `.crt` and `pem` format or `der` format
let cert = if let Ok(cert) = Certificate::from_pem(cert_buf.as_slice()) {
cert
} else if let Ok(cert) = Certificate::from_der(cert_buf.as_slice()) {
cert
} else {
return Err(ActionError::Custom(Box::new(
FetchUrlError::UnknownCertFormat,
)));
};
Ok(cert)
}
#[non_exhaustive]
#[derive(Debug, thiserror::Error)]
pub enum FetchUrlError {
@ -195,6 +181,4 @@ pub enum FetchUrlError {
UnknownUrlScheme,
#[error("Unknown proxy scheme, `https://`, `socks5://`, and `http://` supported")]
UnknownProxyScheme,
#[error("Unknown certificate format, `der` and `pem` supported")]
UnknownCertFormat,
}

View file

@ -1,5 +1,6 @@
use std::path::{Path, PathBuf};
#[cfg(target_os = "linux")]
use std::path::Path;
use std::path::PathBuf;
use tokio::process::Command;
use tracing::{span, Span};
@ -33,6 +34,7 @@ Configure the init to run the Nix daemon
pub struct ConfigureInitService {
init: InitSystem,
start_daemon: bool,
ssl_cert_file: Option<PathBuf>,
}
impl ConfigureInitService {
@ -67,7 +69,18 @@ impl ConfigureInitService {
pub async fn plan(
init: InitSystem,
start_daemon: bool,
ssl_cert_file: Option<PathBuf>,
) -> Result<StatefulAction<Self>, ActionError> {
let ssl_cert_file_path = if let Some(ssl_cert_file) = ssl_cert_file {
Some(
ssl_cert_file
.canonicalize()
.map_err(|e| ActionError::Canonicalize(ssl_cert_file, e))?,
)
} else {
None
};
match init {
#[cfg(target_os = "macos")]
InitSystem::Launchd => {
@ -91,7 +104,12 @@ impl ConfigureInitService {
},
};
Ok(Self { init, start_daemon }.into())
Ok(Self {
init,
start_daemon,
ssl_cert_file: ssl_cert_file_path,
}
.into())
}
}
@ -152,7 +170,11 @@ impl Action for ConfigureInitService {
#[tracing::instrument(level = "debug", skip_all)]
async fn execute(&mut self) -> Result<(), ActionError> {
let Self { init, start_daemon } = self;
let Self {
init,
start_daemon,
ssl_cert_file,
} = self;
match init {
#[cfg(target_os = "macos")]
@ -177,6 +199,18 @@ impl Action for ConfigureInitService {
)
.await?;
if let Some(ssl_cert_file) = ssl_cert_file {
execute_command(
Command::new("launchctl")
.process_group(0)
.arg("setenv")
.arg("NIX_SSL_CERT_FILE")
.arg(format!("{ssl_cert_file:?}"))
.stdin(std::process::Stdio::null()),
)
.await?;
}
if *start_daemon {
execute_command(
Command::new("launchctl")
@ -270,6 +304,28 @@ impl Action for ConfigureInitService {
)
.await?;
if let Some(ssl_cert_file) = ssl_cert_file {
let service_conf_dir_path = PathBuf::from(format!("{SERVICE_DEST}.d"));
tokio::fs::create_dir(&service_conf_dir_path)
.await
.map_err(|e| {
ActionError::CreateDirectory(service_conf_dir_path.clone(), e)
})?;
let service_conf_file_path =
service_conf_dir_path.join("nix-ssl-cert-file.conf");
tokio::fs::write(
service_conf_file_path,
format!(
"\
[Service]\n\
Environment=\"NIX_SSL_CERT_FILE={ssl_cert_file:?}\"\n\
"
),
)
.await
.map_err(|e| ActionError::Write(ssl_cert_file.clone(), e))?;
}
if *start_daemon || socket_was_active {
enable(SOCKET_SRC, true).await?;
} else {
@ -382,6 +438,13 @@ impl Action for ConfigureInitService {
)
.await?;
if self.ssl_cert_file.is_some() {
let service_conf_dir_path = PathBuf::from(format!("{SERVICE_DEST}.d"));
tokio::fs::remove_dir_all(&service_conf_dir_path)
.await
.map_err(|e| ActionError::Remove(service_conf_dir_path.clone(), e))?;
}
tokio::fs::remove_file(TMPFILES_DEST)
.await
.map_err(|e| ActionError::Remove(PathBuf::from(TMPFILES_DEST), e))?;

View file

@ -162,6 +162,7 @@ impl Planner for MyPlanner {
.await?
.into_keys()
.collect::<Vec<_>>(),
self.common.ssl_cert_file.clone(),
))
}
}
@ -197,7 +198,7 @@ use std::{error::Error, process::Output};
use tokio::task::JoinError;
use tracing::Span;
use crate::error::HasExpectedErrors;
use crate::{error::HasExpectedErrors, CertificateError};
/// An action which can be reverted or completed, with an action state
///
@ -305,6 +306,9 @@ pub enum ActionError {
/// A custom error
#[error(transparent)]
Custom(Box<dyn std::error::Error + Send + Sync>),
/// An error to do with certificates
#[error(transparent)]
Certificate(#[from] CertificateError),
/// A child error
#[error("Child action `{0}`")]
Child(ActionTag, #[source] Box<ActionError>),

View file

@ -5,13 +5,14 @@ When enabled with the `diagnostics` feature (default) this module provides autom
That endpoint can be a URL such as `https://our.project.org/nix-installer/diagnostics` or `file:///home/$USER/diagnostic.json` which receives a [`DiagnosticReport`] in JSON format.
*/
use std::time::Duration;
use std::{path::PathBuf, time::Duration};
use os_release::OsRelease;
use reqwest::Url;
use crate::{
action::ActionError, planner::PlannerError, settings::InstallSettingsError, NixInstallerError,
action::ActionError, parse_ssl_cert, planner::PlannerError, settings::InstallSettingsError,
CertificateError, NixInstallerError,
};
/// The static of an action attempt
@ -57,12 +58,18 @@ pub struct DiagnosticData {
triple: String,
is_ci: bool,
endpoint: Option<Url>,
ssl_cert_file: Option<PathBuf>,
/// Generally this includes the [`strum::IntoStaticStr`] representation of the error, we take special care not to include parameters of the error (which may include secrets)
failure_chain: Option<Vec<String>>,
}
impl DiagnosticData {
pub fn new(endpoint: Option<Url>, planner: String, configured_settings: Vec<String>) -> Self {
pub fn new(
endpoint: Option<Url>,
planner: String,
configured_settings: Vec<String>,
ssl_cert_file: Option<PathBuf>,
) -> Self {
let (os_name, os_version) = match OsRelease::new() {
Ok(os_release) => (os_release.name, os_release.version),
Err(_) => ("unknown".into(), "unknown".into()),
@ -78,6 +85,7 @@ impl DiagnosticData {
os_version,
triple: target_lexicon::HOST.to_string(),
is_ci,
ssl_cert_file,
failure_chain: None,
}
}
@ -127,6 +135,7 @@ impl DiagnosticData {
triple,
is_ci,
endpoint: _,
ssl_cert_file: _,
failure_chain,
} = self;
DiagnosticReport {
@ -159,7 +168,15 @@ impl DiagnosticData {
match endpoint.scheme() {
"https" | "http" => {
tracing::debug!("Sending diagnostic to `{endpoint}`");
let client = reqwest::Client::new();
let mut buildable_client = reqwest::Client::builder();
if let Some(ssl_cert_file) = &self.ssl_cert_file {
let ssl_cert = parse_ssl_cert(&ssl_cert_file).await?;
buildable_client = buildable_client.add_root_certificate(ssl_cert);
}
let client = buildable_client
.build()
.map_err(|e| DiagnosticError::Reqwest(e))?;
let res = client
.post(endpoint.clone())
.body(serialized)
@ -206,6 +223,8 @@ pub enum DiagnosticError {
#[source]
serde_json::Error,
),
#[error(transparent)]
Certificate(#[from] CertificateError),
}
pub trait ErrorDiagnostic {

View file

@ -80,7 +80,7 @@ mod plan;
pub mod planner;
pub mod settings;
use std::{ffi::OsStr, process::Output};
use std::{ffi::OsStr, path::Path, process::Output};
use action::{Action, ActionError};
@ -88,6 +88,7 @@ pub use error::NixInstallerError;
pub use plan::InstallPlan;
use planner::BuiltinPlanner;
use reqwest::Certificate;
use tokio::process::Command;
#[tracing::instrument(level = "debug", skip_all, fields(command = %format!("{:?}", command.as_std())))]
@ -111,3 +112,28 @@ fn set_env(k: impl AsRef<OsStr>, v: impl AsRef<OsStr>) {
tracing::trace!("Setting env");
std::env::set_var(k.as_ref(), v.as_ref());
}
async fn parse_ssl_cert(ssl_cert_file: &Path) -> Result<Certificate, CertificateError> {
let cert_buf = tokio::fs::read(ssl_cert_file)
.await
.map_err(|e| CertificateError::Read(ssl_cert_file.to_path_buf(), e))?;
// We actually try them since things could be `.crt` and `pem` format or `der` format
let cert = if let Ok(cert) = Certificate::from_pem(cert_buf.as_slice()) {
cert
} else if let Ok(cert) = Certificate::from_der(cert_buf.as_slice()) {
cert
} else {
return Err(CertificateError::UnknownCertFormat);
};
Ok(cert)
}
#[derive(Debug, thiserror::Error)]
pub enum CertificateError {
#[error(transparent)]
Reqwest(reqwest::Error),
#[error("Read path `{0}`")]
Read(std::path::PathBuf, #[source] std::io::Error),
#[error("Unknown certificate format, `der` and `pem` supported")]
UnknownCertFormat,
}

View file

@ -84,10 +84,14 @@ impl Planner for Linux {
.await
.map_err(PlannerError::Action)?
.boxed(),
ConfigureInitService::plan(self.init.init, self.init.start_daemon)
.await
.map_err(PlannerError::Action)?
.boxed(),
ConfigureInitService::plan(
self.init.init,
self.init.start_daemon,
self.settings.ssl_cert_file.clone(),
)
.await
.map_err(PlannerError::Action)?
.boxed(),
RemoveDirectory::plan(crate::settings::SCRATCH_DIR)
.await
.map_err(PlannerError::Action)?
@ -130,6 +134,7 @@ impl Planner for Linux {
.await?
.into_keys()
.collect::<Vec<_>>(),
self.settings.ssl_cert_file.clone(),
))
}
}

View file

@ -142,10 +142,14 @@ impl Planner for Macos {
.await
.map_err(PlannerError::Action)?
.boxed(),
ConfigureInitService::plan(InitSystem::Launchd, true)
.await
.map_err(PlannerError::Action)?
.boxed(),
ConfigureInitService::plan(
InitSystem::Launchd,
true,
self.settings.ssl_cert_file.clone(),
)
.await
.map_err(PlannerError::Action)?
.boxed(),
RemoveDirectory::plan(crate::settings::SCRATCH_DIR)
.await
.map_err(PlannerError::Action)?
@ -200,6 +204,7 @@ impl Planner for Macos {
.await?
.into_keys()
.collect::<Vec<_>>(),
self.settings.ssl_cert_file.clone(),
))
}
}

View file

@ -78,6 +78,7 @@ impl Planner for MyPlanner {
.await?
.into_keys()
.collect::<Vec<_>>(),
self.common.ssl_cert_file.clone(),
))
}
}

View file

@ -226,10 +226,14 @@ impl Planner for SteamDeck {
.map_err(PlannerError::Action)?
.boxed(),
// Init is required for the steam-deck archetype to make the `/nix` mount
ConfigureInitService::plan(InitSystem::Systemd, true)
.await
.map_err(PlannerError::Action)?
.boxed(),
ConfigureInitService::plan(
InitSystem::Systemd,
true,
self.settings.ssl_cert_file.clone(),
)
.await
.map_err(PlannerError::Action)?
.boxed(),
StartSystemdUnit::plan("ensure-symlinked-units-resolve.service".to_string(), true)
.await
.map_err(PlannerError::Action)?
@ -282,6 +286,7 @@ impl Planner for SteamDeck {
.await?
.into_keys()
.collect::<Vec<_>>(),
self.settings.ssl_cert_file.clone(),
))
}
}

View file

@ -68,7 +68,7 @@ pub struct CommonSettings {
long = "no-modify-profile"
)
)]
pub(crate) modify_profile: bool,
pub modify_profile: bool,
/// Number of build users to create
#[cfg_attr(
@ -81,7 +81,7 @@ pub struct CommonSettings {
global = true
)
)]
pub(crate) nix_build_user_count: u32,
pub nix_build_user_count: u32,
/// The Nix build group name
#[cfg_attr(
@ -93,7 +93,7 @@ pub struct CommonSettings {
global = true
)
)]
pub(crate) nix_build_group_name: String,
pub nix_build_group_name: String,
/// The Nix build group GID
#[cfg_attr(
@ -105,7 +105,7 @@ pub struct CommonSettings {
global = true
)
)]
pub(crate) nix_build_group_id: u32,
pub nix_build_group_id: u32,
/// The Nix build user prefix (user numbers will be postfixed)
#[cfg_attr(
@ -120,7 +120,7 @@ pub struct CommonSettings {
all(target_os = "linux", feature = "cli"),
clap(default_value = "nixbld")
)]
pub(crate) nix_build_user_prefix: String,
pub nix_build_user_prefix: String,
/// The Nix build user base UID (ascending)
#[cfg_attr(
@ -133,7 +133,7 @@ pub struct CommonSettings {
all(target_os = "linux", feature = "cli"),
clap(default_value_t = 30_000)
)]
pub(crate) nix_build_user_id_base: u32,
pub nix_build_user_id_base: u32,
/// The Nix package URL
#[cfg_attr(
@ -170,15 +170,15 @@ pub struct CommonSettings {
default_value = NIX_AARCH64_LINUX_URL,
)
)]
pub(crate) nix_package_url: Url,
pub nix_package_url: Url,
/// The proxy to use (if any), valid proxy bases are `https://$URL`, `http://$URL` and `socks5://$URL`
#[cfg_attr(feature = "cli", clap(long, env = "NIX_INSTALLER_PROXY"))]
pub(crate) proxy: Option<Url>,
pub proxy: Option<Url>,
/// An SSL cert to use (if any), used for fetching Nix and sets `NIX_SSL_CERT_FILE` for Nix
#[cfg_attr(feature = "cli", clap(long, env = "NIX_INSTALLER_SSL_CERT_FILE"))]
pub(crate) ssl_cert_file: Option<PathBuf>,
pub ssl_cert_file: Option<PathBuf>,
/// Extra configuration lines for `/etc/nix.conf`
#[cfg_attr(feature = "cli", clap(long, action = ArgAction::Set, num_args = 0.., value_delimiter = ',', env = "NIX_INSTALLER_EXTRA_CONF", global = true))]
@ -195,7 +195,7 @@ pub struct CommonSettings {
env = "NIX_INSTALLER_FORCE"
)
)]
pub(crate) force: bool,
pub force: bool,
#[cfg(feature = "diagnostics")]
/// The URL or file path for an installation diagnostic to be sent
@ -379,69 +379,6 @@ async fn linux_detect_systemd_started() -> bool {
started
}
// Builder Pattern
impl CommonSettings {
/// Number of build users to create
pub fn nix_build_user_count(&mut self, count: u32) -> &mut Self {
self.nix_build_user_count = count;
self
}
/// Modify the user profile to automatically load nix
pub fn modify_profile(&mut self, toggle: bool) -> &mut Self {
self.modify_profile = toggle;
self
}
/// The Nix build group name
pub fn nix_build_group_name(&mut self, val: String) -> &mut Self {
self.nix_build_group_name = val;
self
}
/// The Nix build group GID
pub fn nix_build_group_id(&mut self, count: u32) -> &mut Self {
self.nix_build_group_id = count;
self
}
/// The Nix build user prefix (user numbers will be postfixed)
pub fn nix_build_user_prefix(&mut self, val: String) -> &mut Self {
self.nix_build_user_prefix = val;
self
}
/// The Nix build user base UID (ascending)
pub fn nix_build_user_id_base(&mut self, count: u32) -> &mut Self {
self.nix_build_user_id_base = count;
self
}
/// The Nix package URL
pub fn nix_package_url(&mut self, url: Url) -> &mut Self {
self.nix_package_url = url;
self
}
/// Extra configuration lines for `/etc/nix.conf`
pub fn extra_conf(&mut self, extra_conf: Vec<String>) -> &mut Self {
self.extra_conf = extra_conf;
self
}
/// If `nix-installer` should forcibly recreate files it finds existing
pub fn force(&mut self, force: bool) -> &mut Self {
self.force = force;
self
}
#[cfg(feature = "diagnostics")]
/// The URL or file path for an [`DiagnosticReport`][crate::diagnostics::DiagnosticReport] to be sent
pub fn diagnostic_endpoint(&mut self, diagnostic_endpoint: Option<Url>) -> &mut Self {
self.diagnostic_endpoint = diagnostic_endpoint;
self
}
}
#[serde_with::serde_as]
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
#[cfg_attr(feature = "cli", derive(clap::Parser))]
@ -456,7 +393,7 @@ pub struct InitSettings {
all(target_os = "linux", feature = "cli"),
clap(default_value_t = InitSystem::Systemd)
)]
pub(crate) init: InitSystem,
pub init: InitSystem,
/// Start the daemon (if not `--init none`)
#[cfg_attr(
@ -470,7 +407,7 @@ pub struct InitSettings {
long = "no-start-daemon"
)
)]
pub(crate) start_daemon: bool,
pub start_daemon: bool,
}
impl InitSettings {