Skip to content

Commit db796de

Browse files
Implement posture checks (#871)
* nix flake update * initial work on posture checks on old UI * nix flake update * posture check for non-mfa locations * posture checks during MFA connection flow * cargo update; extend glib trivyignore * fix clippy issue * fix typing * update protos, restructure protos module * fix proto imports; only windows gets postures from service * remove console.logs * allow dead code * fix location id; better error message * whitelist new tauri commands * fix windows compilation * import Id * remove unused command argument * post_with_headers helper * fix windows proto import * update proto submodule * move non-mfa posture connection fully to backend
1 parent 56d8fc0 commit db796de

23 files changed

Lines changed: 237 additions & 63 deletions

File tree

flake.lock

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

nix/shell.nix

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ in
3232
trunk
3333
sqlx-cli
3434
vtsls
35+
trivy
36+
just
3537
];
3638

3739
shellHook = with pkgs; ''

src-tauri/permissions/default.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,5 @@ commands.allow = [
3636
"swap_to_old_ui",
3737
"all_active_connections",
3838
"disconnect_locations",
39+
"get_posture_data",
3940
]

src-tauri/proto

src-tauri/src/bin/defguard-client.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ fn main() {
182182
command_set_app_config,
183183
get_provisioning_config,
184184
get_platform_header,
185+
get_posture_data,
185186
set_location_mfa_method,
186187
open_new_ui_window,
187188
open_old_ui_window,

src-tauri/src/commands.rs

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,18 @@ use crate::{
2929
},
3030
DB_POOL,
3131
},
32-
enterprise::{periodic::config::poll_instance, provisioning::ProvisioningConfig},
32+
enterprise::{
33+
periodic::config::poll_instance, posture::authorize_posture_session,
34+
provisioning::ProvisioningConfig,
35+
},
3336
error::Error,
3437
events::EventKey,
3538
log_watcher::{
3639
global_log_watcher::{spawn_global_log_watcher_task, stop_global_log_watcher_task},
3740
service_log_watcher::stop_log_watcher_task,
3841
},
3942
proto::defguard::client_types::DeviceConfigResponse,
43+
service::proto::defguard::enterprise::posture::v2::DevicePostureData,
4044
tray::{configure_tray_icon, reload_tray_menu},
4145
utils::{
4246
construct_platform_header, disconnect_interface, get_location_interface_details,
@@ -50,7 +54,7 @@ use crate::{
5054
use crate::{
5155
service::{
5256
client::DAEMON_CLIENT,
53-
proto::{
57+
proto::defguard::client::v1::{
5458
DeleteServiceLocationsRequest, RemoveInterfaceRequest, SaveServiceLocationsRequest,
5559
},
5660
},
@@ -72,6 +76,11 @@ pub async fn connect(
7276
"Identified location with ID {location_id} as \"{}\", handling connection.",
7377
location.name
7478
);
79+
let preshared_key = if location.posture_check_required && preshared_key.is_none() {
80+
Some(authorize_posture_session(&location).await?)
81+
} else {
82+
preshared_key
83+
};
7584
handle_connection_for_location(&location, preshared_key, &handle).await?;
7685
reload_tray_menu(&handle).await;
7786
info!("Connected to location {location}");
@@ -490,6 +499,7 @@ pub struct LocationInfo {
490499
pub pubkey: String,
491500
pub network_id: Id,
492501
pub location_mfa_mode: LocationMfaMode,
502+
pub posture_check_required: bool,
493503
pub mfa_method: Option<LocationMfaMethod>,
494504
}
495505

@@ -543,6 +553,7 @@ pub async fn all_locations(instance_id: Id) -> Result<Vec<LocationInfo>, Error>
543553
pubkey: location.pubkey,
544554
network_id: location.network_id,
545555
location_mfa_mode: location.location_mfa_mode,
556+
posture_check_required: location.posture_check_required,
546557
mfa_method: location.mfa_method,
547558
};
548559
location_info.push(info);
@@ -1521,6 +1532,28 @@ pub fn get_platform_header() -> String {
15211532
construct_platform_header()
15221533
}
15231534

1535+
#[tauri::command(async)]
1536+
#[cfg(not(windows))]
1537+
pub async fn get_posture_data() -> Result<DevicePostureData, Error> {
1538+
debug!("Received a command to prepare posture report");
1539+
Ok(DevicePostureData::new())
1540+
}
1541+
1542+
#[tauri::command(async)]
1543+
#[cfg(windows)]
1544+
pub async fn get_posture_data() -> Result<DevicePostureData, Error> {
1545+
debug!("Received a command to prepare posture report");
1546+
DAEMON_CLIENT
1547+
.clone()
1548+
.get_posture_data(tonic::Request::new(()))
1549+
.await
1550+
.map(|response| response.into_inner())
1551+
.map_err(|err| {
1552+
error!("Failed to get posture data from the daemon: {err}");
1553+
Error::InternalError(format!("Failed to get posture data from the daemon: {err}"))
1554+
})
1555+
}
1556+
15241557
#[derive(Debug, Serialize)]
15251558
pub struct ActiveConnectionSummary {
15261559
pub id: Id,

src-tauri/src/enterprise/inspector/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use std::{env::consts::OS, error::Error, fmt};
1212
use sysinfo::System;
1313

1414
use crate::{
15-
proto::defguard::enterprise::posture::v2::{
15+
service::proto::defguard::enterprise::posture::v2::{
1616
bool_check, string_check, BoolCheck, DevicePostureData, StringCheck, UnavailableReason,
1717
},
1818
VERSION,

src-tauri/src/enterprise/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
pub mod inspector;
22
pub mod models;
33
pub mod periodic;
4+
pub mod posture;
45
pub mod provisioning;
56
pub mod service_locations;

src-tauri/src/enterprise/periodic/config.rs

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use std::{
66
time::Duration,
77
};
88

9-
use reqwest::{Client, StatusCode};
9+
use reqwest::StatusCode;
1010
use serde::Serialize;
1111
use sqlx::{Sqlite, Transaction};
1212
use tauri::{AppHandle, Emitter, Url};
@@ -24,13 +24,11 @@ use crate::{
2424
proto::defguard::client_types::{
2525
DeviceConfigResponse, InstanceInfoRequest, InstanceInfoResponse,
2626
},
27-
utils::construct_platform_header,
28-
CLIENT_PLATFORM_HEADER, CLIENT_VERSION_HEADER, MIN_CORE_VERSION, MIN_PROXY_VERSION,
29-
PKG_VERSION,
27+
utils::post_with_headers,
28+
MIN_CORE_VERSION, MIN_PROXY_VERSION,
3029
};
3130

3231
const INTERVAL_SECONDS: Duration = Duration::from_secs(30);
33-
const HTTP_REQ_TIMEOUT: Duration = Duration::from_secs(5);
3432
static POLLING_ENDPOINT: &str = "/api/v1/poll";
3533

3634
/// Periodically retrieves and updates configuration for all [`Instance`]s.
@@ -138,14 +136,7 @@ pub async fn poll_instance(
138136
instance.proxy_url
139137
))
140138
})?;
141-
let response = Client::new()
142-
.post(url)
143-
.json(&request)
144-
.header(CLIENT_VERSION_HEADER, PKG_VERSION)
145-
.header(CLIENT_PLATFORM_HEADER, construct_platform_header())
146-
.timeout(HTTP_REQ_TIMEOUT)
147-
.send()
148-
.await;
139+
let response = post_with_headers(url, &request).await;
149140
let response = response.map_err(|err| {
150141
Error::InternalError(format!(
151142
"HTTP request failed for instance {}({}), url: {}, {err}",
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
use reqwest::StatusCode;
2+
use serde::Deserialize;
3+
4+
use crate::{
5+
database::{
6+
models::{instance::Instance, location::Location, wireguard_keys::WireguardKeys, Id},
7+
DB_POOL,
8+
},
9+
error::Error,
10+
service::proto::defguard::enterprise::posture::v2::{
11+
DevicePostureCheckRequest, DevicePostureCheckResponse, DevicePostureData,
12+
},
13+
utils::post_with_headers,
14+
};
15+
16+
const POSTURE_ENDPOINT: &str = "/api/v1/posture/connect";
17+
18+
/// Collects device posture data, sends it to the proxy, and returns the runtime preshared key.
19+
pub async fn authorize_posture_session(location: &Location<Id>) -> Result<String, Error> {
20+
let instance = Instance::find_by_id(&*DB_POOL, location.instance_id)
21+
.await?
22+
.ok_or(Error::NotFound)?;
23+
24+
let keys = WireguardKeys::find_by_instance_id(&*DB_POOL, location.instance_id)
25+
.await?
26+
.ok_or_else(|| {
27+
Error::ResourceNotFound(format!(
28+
"WireGuard keys not found for instance {}",
29+
location.instance_id
30+
))
31+
})?;
32+
33+
let posture_data = DevicePostureData::new();
34+
35+
let request = DevicePostureCheckRequest {
36+
location_id: location.network_id,
37+
pubkey: keys.pubkey,
38+
device_posture_data: Some(posture_data),
39+
};
40+
41+
let proxy_url = tauri::Url::parse(&instance.proxy_url)
42+
.map_err(|e| Error::InternalError(format!("Invalid proxy URL: {e}")))?
43+
.join(POSTURE_ENDPOINT)
44+
.map_err(|e| Error::InternalError(format!("Failed to build posture URL: {e}")))?;
45+
46+
debug!("Sending posture check request to {proxy_url}");
47+
let response = post_with_headers(proxy_url, &request)
48+
.await
49+
.map_err(|e| Error::HttpError(e.to_string()))?;
50+
51+
match response.status() {
52+
StatusCode::OK => {
53+
let body: DevicePostureCheckResponse = response
54+
.json()
55+
.await
56+
.map_err(|e| Error::HttpError(e.to_string()))?;
57+
info!("Posture check approved for location {}", location.id);
58+
Ok(body.preshared_key)
59+
}
60+
StatusCode::FORBIDDEN => {
61+
#[derive(Deserialize)]
62+
struct PostureRejection {
63+
error: String,
64+
}
65+
let body: PostureRejection = response
66+
.json()
67+
.await
68+
.map_err(|e| Error::HttpError(e.to_string()))?;
69+
error!(
70+
"Posture check rejected for location {}: {}",
71+
location.id, body.error
72+
);
73+
Err(Error::PostureCheckFailed(body.error))
74+
}
75+
status => Err(Error::HttpError(format!(
76+
"Unexpected proxy response: {status}"
77+
))),
78+
}
79+
}

0 commit comments

Comments
 (0)