manta_server/server/common/
vault.rs

1//! Vault client used by handlers that need backend-specific secrets
2//! (Gitea token for `create_session`, Kubernetes credentials for the
3//! console and log-streaming handlers).
4
5/// Thin Vault HTTP client. Authenticates via OIDC/JWT against a
6/// per-site `jwt-manta-<site>` role, then reads K/V v2 secrets under
7/// `manta/data/<...>`.
8pub mod http_client {
9
10  use manta_backend_dispatcher::error::Error;
11  use serde_json::{Value, json};
12
13  /// Vault API version prefix.
14  const VAULT_API_PREFIX: &str = "/v1";
15
16  /// Vault KV secret path prefix for manta.
17  const VAULT_SECRET_PATH_PREFIX: &str = "manta/data";
18
19  /// Vault role name used for JWT authentication.
20  const VAULT_ROLE: &str = "manta";
21
22  /// Authenticate to Vault using a JWT token and return
23  /// a Vault client token.
24  pub async fn auth_oidc_jwt(
25    vault_base_url: &str,
26    shasta_token: &str,
27    site_name: &str,
28  ) -> Result<String, Error> {
29    let role = VAULT_ROLE;
30
31    let client = reqwest::Client::builder().build()?;
32
33    let api_url = format!(
34      "{vault_base_url}{VAULT_API_PREFIX}/auth/jwt-manta-{site_name}/login"
35    );
36
37    tracing::debug!("Accessing/login to {}", api_url);
38
39    let request_payload = json!({ "jwt": shasta_token, "role": role });
40
41    let resp = client
42      .post(api_url)
43      .header("X-Vault-Request", "true")
44      .json(&request_payload)
45      .send()
46      .await?
47      .error_for_status()?;
48
49    let resp_value = resp.json::<Value>().await?;
50    let client_token = resp_value["auth"]
51      .get("client_token")
52      .and_then(Value::as_str)
53      .ok_or_else(|| {
54        Error::MissingField(
55          "Vault auth response missing 'client_token' field".to_string(),
56        )
57      })?;
58    Ok(client_token.to_string())
59  }
60
61  /// Fetch a secret from Vault's KV store at `secret_path`.
62  pub async fn fetch_secret(
63    vault_auth_token: &str,
64    vault_base_url: &str,
65    secret_path: &str,
66  ) -> Result<Value, Error> {
67    let client = reqwest::Client::builder().build()?;
68
69    let api_url = vault_base_url.to_owned() + secret_path;
70
71    tracing::debug!("Vault url to fetch VCS secrets is '{}'", api_url);
72
73    let resp = client
74      .get(api_url)
75      .header("X-Vault-Token", vault_auth_token)
76      .send()
77      .await?
78      .error_for_status()?;
79
80    let secret_value: Value = resp.json().await?;
81    Ok(secret_value["data"].clone())
82  }
83
84  /// Retrieve the Gitea VCS token from Vault.
85  pub async fn fetch_shasta_vcs_token(
86    shasta_token: &str,
87    vault_base_url: &str,
88    site_name: &str,
89  ) -> Result<String, Error> {
90    let vault_token =
91      auth_oidc_jwt(vault_base_url, shasta_token, site_name).await?;
92
93    let vault_secret_path = format!("{VAULT_SECRET_PATH_PREFIX}/{site_name}");
94
95    let vault_secret = fetch_secret(
96      &vault_token,
97      vault_base_url,
98      &format!("{VAULT_API_PREFIX}/{vault_secret_path}/vcs"),
99    )
100    .await?;
101
102    let vcs_token = vault_secret["data"]
103      .get("token")
104      .and_then(Value::as_str)
105      .ok_or_else(|| {
106      Error::MissingField(
107        "Vault secret response missing 'token' field".to_string(),
108      )
109    })?;
110
111    Ok(vcs_token.to_string())
112  }
113
114  /// Retrieve Kubernetes secrets (API URL, token, CA cert)
115  /// from Vault.
116  pub async fn fetch_shasta_k8s_secrets_from_vault(
117    vault_base_url: &str,
118    site_name: &str,
119    shasta_token: &str,
120  ) -> Result<Value, Error> {
121    let vault_token =
122      auth_oidc_jwt(vault_base_url, shasta_token, site_name).await?;
123
124    let vault_secret_path = format!("{VAULT_SECRET_PATH_PREFIX}/{site_name}");
125
126    let secret = fetch_secret(
127      &vault_token,
128      vault_base_url,
129      &format!("{VAULT_API_PREFIX}/{vault_secret_path}/k8s"),
130    )
131    .await?;
132
133    Ok(secret["data"].clone())
134  }
135}