wasmcloud_host/
workload_identity.rs

1use std::sync::Arc;
2
3#[cfg(target_family = "windows")]
4use anyhow::{bail, Result};
5#[cfg(unix)]
6use anyhow::{Context as _, Result};
7use nkeys::KeyPair;
8
9// TODO(joonas): Figure out better naming here
10const AUTH_SERVICE_AUDIENCE_ENV: &str = "WASMCLOUD_WORKLOAD_IDENTITY_AUTH_SERVICE_AUDIENCE";
11
12/// WorkloadIdentityConfig is used by the experimental workload-identity feature
13#[derive(Clone, Default, Debug)]
14pub struct WorkloadIdentityConfig {
15    /// auth_service_audience represents the value expected by the Auth Callout Service,
16    /// typically this should look something like "spiffe://wasmcloud.dev/auth-callout"
17    pub auth_service_audience: String,
18}
19
20impl WorkloadIdentityConfig {
21    /// Fetch workload identity configuration from environment variables
22    #[cfg(unix)]
23    pub fn from_env() -> Result<Self> {
24        // TODO(joonas): figure out better naming here. maybe this should be interpolated from a trust domain?
25        // This needs to follow format like: "spiffe://{spiffe_trust_domain}/{nats_auth_callout_service}"
26        let auth_service_audience = std::env::var(AUTH_SERVICE_AUDIENCE_ENV)
27            .context("workload identity auth callout audience environment variable is missing")?;
28
29        Ok(Self {
30            auth_service_audience,
31        })
32    }
33
34    #[cfg(target_family = "windows")]
35    pub fn from_env() -> Result<Self> {
36        anyhow::bail!("workload identity is not supported on Windows")
37    }
38}
39
40#[cfg(unix)]
41pub(crate) async fn setup_workload_identity_nats_connect_options(
42    jwt: Option<&String>,
43    key: Option<Arc<KeyPair>>,
44    wid_cfg: WorkloadIdentityConfig,
45) -> anyhow::Result<async_nats::ConnectOptions> {
46    let wid_cfg = Arc::new(wid_cfg);
47    let jwt = jwt.map(String::to_string).map(Arc::new);
48    let key = key.clone();
49
50    // Return an auth callback that'll get called any time the
51    // NATS connection needs to be (re-)established. This is
52    // necessary to ensure that we always provide a recently
53    // issued JWT-SVID.
54    Ok(
55        async_nats::ConnectOptions::with_auth_callback(move |nonce| {
56            let key = key.clone();
57            let jwt = jwt.clone();
58            let wid_cfg = wid_cfg.clone();
59
60            let fetch_svid_handle = tokio::spawn(async move {
61                let mut client = spiffe::WorkloadApiClient::default()
62                    .await
63                    .map_err(async_nats::AuthError::new)?;
64                client
65                    .fetch_jwt_svid(&[wid_cfg.auth_service_audience.as_str()], None)
66                    .await
67                    .map_err(async_nats::AuthError::new)
68            });
69
70            async move {
71                let svid = fetch_svid_handle
72                    .await
73                    .map_err(async_nats::AuthError::new)?
74                    .map_err(async_nats::AuthError::new)?;
75
76                let mut auth = async_nats::Auth::new();
77                if let Some(key) = key {
78                    let signature = key.sign(&nonce).map_err(async_nats::AuthError::new)?;
79                    auth.signature = Some(signature);
80                }
81                if let Some(jwt) = jwt {
82                    auth.jwt = Some(jwt.to_string());
83                }
84                auth.token = Some(svid.token().into());
85                Ok(auth)
86            }
87        })
88        .name("wasmbus"),
89    )
90}
91
92#[cfg(target_family = "windows")]
93pub(crate) async fn setup_workload_identity_nats_connect_options(
94    jwt: Option<&String>,
95    key: Option<Arc<KeyPair>>,
96    wid_cfg: WorkloadIdentityConfig,
97) -> anyhow::Result<async_nats::ConnectOptions> {
98    bail!("workload identity is not supported on Windows")
99}