Skip to main content

isb_core/egress/
mod.rs

1//! Per-sandbox egress policy: an allowlist of `host[:port]` a sandbox may
2//! reach, and secrets that never enter the guest.
3//!
4//! The pieces, in the order a connection meets them (docs/guides/egress.md):
5//!
6//! - [`policy`]: the `egress:` field, validated, and the matching rules;
7//! - [`plumb`]: the incus side, a bridge, an ACL and a filtering dnsmasq per
8//!   sandbox, so the guest can reach nothing but the proxy;
9//! - [`ca`]: the per-sandbox CA the proxy terminates TLS with, and the
10//!   guest's trust in it;
11//! - the proxy itself, which enforces the policy, is the `isb-egress` crate,
12//!   run by `isb serve`.
13
14pub mod ca;
15pub mod plumb;
16pub mod policy;
17
18pub use plumb::Plumbing;
19pub use policy::{EgressSecretSpec, EgressSpec, Entry, HostPattern, Policy, SecretBinding};
20
21use crate::client::Client;
22use crate::error::Result;
23use plumb::{KEY_POLICY, Props};
24
25/// What a sandbox's `egress:` adds to its resolved instance.
26#[derive(Debug, Clone)]
27pub struct Contribution {
28    pub plumbing: Plumbing,
29    pub nic: Props,
30    pub config: Props,
31}
32
33/// Validate `spec` for instance `instance` in incus project `project`, and
34/// say what it adds: the NIC, and the config keys (the stored policy, the
35/// placeholders, and the CA settings when a secret is involved).
36pub fn contribute(spec: &EgressSpec, project: &str, instance: &str) -> Result<Contribution> {
37    let network = plumb::network_name(project, instance);
38    let policy = Policy::from_spec(spec, &network)?;
39    let plumbing = Plumbing::new(project, instance, policy);
40    let mut config = Props::new();
41    config.insert(KEY_POLICY.into(), plumbing.policy_json());
42    config.insert(plumb::KEY_FOR.into(), plumbing.owner());
43    for s in &plumbing.policy.secrets {
44        config.insert(format!("environment.{}", s.env), s.placeholder.clone());
45    }
46    if !plumbing.policy.secrets.is_empty() {
47        config.extend(ca::guest_env());
48    }
49    Ok(Contribution {
50        nic: plumbing.nic(),
51        plumbing,
52        config,
53    })
54}
55
56/// Before the instance is created or changed: the bridge, the ACL and the CA.
57pub fn before_apply(client: &Client, p: &Plumbing, report: &mut dyn FnMut(&str)) -> Result<()> {
58    // The CA first: the proxy looks for it as soon as the network exists.
59    if !p.policy.secrets.is_empty() {
60        ca::Ca::ensure(&p.network)?;
61    }
62    plumb::prepare(client, p, report)?;
63    Ok(())
64}
65
66/// Once the instance runs: make the guest trust the CA, when there are secrets.
67pub fn after_ready(client: &Client, p: &Plumbing) -> Result<()> {
68    if p.policy.secrets.is_empty() {
69        return Ok(());
70    }
71    match ca::Ca::load(&p.network)? {
72        Some(ca) => ca::install(client, &p.instance, &ca),
73        None => Err(crate::error::Error::invalid(format!(
74            "{}: the egress CA is missing from this host's state directory; recreate the sandbox",
75            p.instance
76        ))),
77    }
78}