Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion crates/devolutions-agent-shared/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use std::env;
use camino::Utf8PathBuf;
use cfg_if::cfg_if;
pub use date_version::{DateVersion, DateVersionError};
pub use restricted_file::write_restricted_file;
pub use restricted_file::{create_restricted_directory, write_restricted_file, write_restricted_file_atomic};
pub use update_manifest::{
InstalledProductUpdateInfo, ProductUpdateInfo, ProductUpdateInfoV1, UPDATE_MANIFEST_V2_MINOR_VERSION,
UpdateManifest, UpdateManifestV1, UpdateManifestV2, UpdateProductKey, UpdateSchedule, VersionMajorV2,
Expand Down
106 changes: 96 additions & 10 deletions crates/devolutions-agent-shared/src/restricted_file.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,80 @@
use anyhow::Context as _;
use camino::Utf8Path;

pub fn write_restricted_file_atomic(path: &Utf8Path, contents: &str) -> anyhow::Result<()> {
use std::io::Write as _;

let parent = path.parent().context("restricted file requires a parent directory")?;
let mut temporary = tempfile::Builder::new()
.prefix(".identity-")
.make_in(parent, |candidate| {
let candidate = Utf8Path::from_path(candidate)
.ok_or_else(|| std::io::Error::other("non-UTF-8 restricted file path"))?;
create_restricted_file(candidate).map_err(std::io::Error::other)
})
.context("create restricted temporary file")?;
temporary
.write_all(contents.as_bytes())
.context("write restricted temporary file")?;
temporary
.as_file()
.sync_all()
.context("sync restricted temporary file")?;
temporary
.into_temp_path()
.persist(path)
.map_err(|error| error.error)
.context("replace restricted file")?;
#[cfg(unix)]
std::fs::File::open(parent)
.and_then(|directory| directory.sync_all())
.context("sync restricted file directory")?;
Ok(())
}

pub fn create_restricted_directory(path: &Utf8Path) -> anyhow::Result<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).context("create restricted directory parent")?;
}
match std::fs::symlink_metadata(path) {
Ok(metadata) => anyhow::ensure!(
metadata.is_dir() && !metadata.file_type().is_symlink(),
"restricted directory must be a real directory"
),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
Err(error) => return Err(error).context("inspect restricted directory"),
}

#[cfg(unix)]
{
use std::os::unix::fs::{DirBuilderExt as _, PermissionsExt as _};
if !path.exists() {
std::fs::DirBuilder::new()
.mode(0o700)
.create(path)
.context("create restricted directory")?;
}
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o700))
.context("restrict directory permissions")?;
}
#[cfg(windows)]
{
use win_api_wrappers::raw::Win32::Security::Authorization::SE_FILE_OBJECT;
use win_api_wrappers::security::acl::set_named_security_info;
use win_api_wrappers::str::U16CString;
let attributes = restricted_attributes(true)?;
if !path.exists() {
win_api_wrappers::fs::create_directory(path.as_std_path(), Some(&attributes))
.context("create restricted directory")?;
}
let name = U16CString::from_str(path.as_str()).context("encode restricted directory path")?;
let dacl = restricted_dacl(true)?;
set_named_security_info(&name, SE_FILE_OBJECT, None, None, Some(&dacl), None)
.context("restrict directory permissions")?;
}
Ok(())
}

pub fn write_restricted_file(path: &Utf8Path, contents: &str) -> anyhow::Result<()> {
use std::io::Write as _;

Expand Down Expand Up @@ -28,22 +102,32 @@ fn create_restricted_file(path: &Utf8Path) -> anyhow::Result<std::fs::File> {

#[cfg(windows)]
fn create_restricted_file(path: &Utf8Path) -> anyhow::Result<std::fs::File> {
let attributes = restricted_attributes(false)?;
win_api_wrappers::fs::create_file(path.as_std_path(), Some(&attributes))
.with_context(|| format!("create file {path}"))
}

#[cfg(windows)]
fn restricted_dacl(inherit: bool) -> anyhow::Result<win_api_wrappers::security::acl::InheritableAcl> {
use win_api_wrappers::identity::sid::Sid;
use win_api_wrappers::raw::Win32::Security;
use win_api_wrappers::raw::Win32::Security::Authorization::GRANT_ACCESS;
use win_api_wrappers::raw::Win32::Storage::FileSystem::{
DELETE, FILE_ALL_ACCESS, FILE_GENERIC_READ, FILE_GENERIC_WRITE,
};
use win_api_wrappers::security::acl::{Acl, ExplicitAccess, InheritableAcl, InheritableAclKind, Trustee};
use win_api_wrappers::security::attributes::SecurityAttributesInit;
use win_api_wrappers::token::Token;

let modify = FILE_GENERIC_READ.0 | FILE_GENERIC_WRITE.0 | DELETE.0;

let entry = |access_permissions, sid| ExplicitAccess {
access_permissions,
access_mode: GRANT_ACCESS,
inheritance: Security::NO_INHERITANCE,
inheritance: if inherit {
Security::OBJECT_INHERIT_ACE | Security::CONTAINER_INHERIT_ACE
} else {
Security::NO_INHERITANCE
},
trustee: Trustee::Sid(sid),
};

Expand All @@ -62,19 +146,21 @@ fn create_restricted_file(path: &Utf8Path) -> anyhow::Result<std::fs::File> {
),
];

let dacl = InheritableAcl {
Ok(InheritableAcl {
kind: InheritableAclKind::Protected,
acl: Acl::new()
.and_then(|acl| acl.set_entries(&entries))
.context("build restricted DACL")?,
};
})
}

#[cfg(windows)]
fn restricted_attributes(inherit: bool) -> anyhow::Result<win_api_wrappers::security::attributes::SecurityAttributes> {
use win_api_wrappers::security::attributes::SecurityAttributesInit;

let attributes = SecurityAttributesInit {
dacl: Some(dacl),
Ok(SecurityAttributesInit {
dacl: Some(restricted_dacl(inherit)?),
..Default::default()
}
.init();

win_api_wrappers::fs::create_file(path.as_std_path(), Some(&attributes))
.with_context(|| format!("create file {path}"))
.init())
}
2 changes: 2 additions & 0 deletions devolutions-agent/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,12 @@ prost-types = "0.13"
quinn = "0.11"
rand = "0.8" # FIXME(@CBenoit): maybe we don't need this crate
rcgen = { version = "0.13", features = ["pem"] }
ring = "0.17"
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls-native-roots", "http2", "socks", "json"] }
rustls = { version = "0.23", default-features = false, features = ["std", "ring"] }
rustls-pemfile = "2.2"
rustls-pki-types = "1"
rustls-webpki = { version = "0.103", default-features = false, features = ["std", "ring"] }
sha2 = "0.10"
serde_json = "1"
serde = { version = "1", features = ["derive"] }
Expand Down
38 changes: 37 additions & 1 deletion devolutions-agent/docker/devolutions-agent-entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,42 @@ json_escape() {
printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'
}

if [ -n "${PSU_SERVER_URL:-}" ] && [ -n "${PSU_APP_TOKEN:-}" ]; then
if [ -n "${PSU_DEVICE_ROOT_THUMBPRINT:-}" ]; then
if [ -z "${PSU_SERVER_URL:-}" ] || [ -z "${PSU_DEVICE_STATE_DIRECTORY:-}" ]; then
echo "PSU device mode requires PSU_SERVER_URL and PSU_DEVICE_STATE_DIRECTORY on a persistent writable volume" >&2
exit 1
fi
if [ -n "${PSU_APP_TOKEN:-}" ]; then
echo "PSU_APP_TOKEN must not be set in PSU device mode" >&2
exit 1
fi
token_config=''
if [ -n "${PSU_ENROLLMENT_TOKEN:-}" ]; then
token_config=", \"EnrollmentToken\": \"$(json_escape "${PSU_ENROLLMENT_TOKEN}")\""
fi
hardware_config=''
if [ -n "${PSU_DEVICE_HARDWARE_ID:-}" ]; then
hardware_config=", \"HardwareId\": \"$(json_escape "${PSU_DEVICE_HARDWARE_ID}")\""
fi
psu_agent_config=$(cat <<EOF
"PsuAgent": {
"Enabled": true,
"ServerUrl": "$(json_escape "${PSU_SERVER_URL}")",
"DisplayName": "$(json_escape "${PSU_DISPLAY_NAME:-Devolutions Agent Linux}")",
"DeviceEnrollment": {
"RootThumbprint": "$(json_escape "${PSU_DEVICE_ROOT_THUMBPRINT}")",
"StateDirectory": "$(json_escape "${PSU_DEVICE_STATE_DIRECTORY}")"${token_config}${hardware_config}
},
"PowerShell": {
"ExecutablePath": "$(json_escape "${PSU_POWERSHELL_EXECUTABLE:-${POWERSHELL_EXECUTABLE:-pwsh}}")"
}
}
EOF
)
elif [ -n "${PSU_ENROLLMENT_TOKEN:-}" ] || [ -n "${PSU_DEVICE_STATE_DIRECTORY:-}" ] || [ -n "${PSU_DEVICE_HARDWARE_ID:-}" ]; then
echo "PSU device settings require PSU_DEVICE_ROOT_THUMBPRINT" >&2
exit 1
elif [ -n "${PSU_SERVER_URL:-}" ] && [ -n "${PSU_APP_TOKEN:-}" ]; then
psu_agent_config=$(cat <<EOF
"PsuAgent": {
"Enabled": true,
Expand All @@ -28,6 +63,7 @@ else
psu_agent_config=' "PsuAgent": { "Enabled": false }'
fi

umask 077
cat > "${DAGENT_CONFIG_PATH}/agent.json" <<EOF
{
"Updater": {
Expand Down
45 changes: 45 additions & 0 deletions devolutions-agent/proto/psu_agent.proto
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,51 @@ service AgentControl {
rpc Connect(stream AgentMessage) returns (stream ServerMessage);
}

service AgentEnrollment {
rpc GetTrustAnchor(TrustAnchorRequest) returns (TrustAnchorResponse);
rpc Enroll(EnrollRequest) returns (EnrollResponse);
rpc Renew(RenewRequest) returns (EnrollResponse);
}

message TrustAnchorRequest {
string nonce = 1;
}

message TrustAnchorResponse {
bytes root_certificate = 1;
string root_thumbprint = 2;
string server_assertion = 3;
}

message EnrollRequest {
bytes csr = 1;
string machine_name = 2;
string os = 3;
string architecture = 4;
string agent_version = 5;
string requested_display_name = 6;
string hardware_id = 7;
}

enum EnrollmentStatus {
ENROLLMENT_STATUS_UNSPECIFIED = 0;
ENROLLMENT_STATUS_APPROVED = 1;
ENROLLMENT_STATUS_PENDING_APPROVAL = 2;
}

message EnrollResponse {
string device_id = 1;
bytes certificate = 2;
repeated bytes chain = 3;
EnrollmentStatus status = 4;
string certificate_thumbprint = 5;
google.protobuf.Timestamp not_after = 6;
}

message RenewRequest {
bytes csr = 1;
}

message AgentMessage {
string request_id = 1;
string agent_id = 2;
Expand Down
Loading