Skip to content

Commit 31a8d42

Browse files
committed
feat: support passing environment variables to the enclave
This is useful when running in Kubernetes to pass configuration from ConfigMaps and Secrets. The new `env` key in the manifest specifies the list of names of environment variables that should be passed to the enclave.
1 parent c8f9e1f commit 31a8d42

5 files changed

Lines changed: 127 additions & 9 deletions

File tree

enclaver/src/bin/odyn/launcher.rs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use log::debug;
33
use nix::sys::signal::Signal;
44
use nix::sys::wait::{WaitPidFlag, WaitStatus};
55
use nix::unistd::Pid;
6+
use std::collections::HashMap;
67
use std::ffi::OsString;
78
use std::os::unix::process::CommandExt;
89
use std::process::Command;
@@ -28,12 +29,17 @@ impl std::fmt::Display for ExitStatus {
2829
}
2930

3031
// runs the child and reaps all of its children as well
31-
pub fn run_child(argv: &[OsString], creds: &Credentials) -> Result<ExitStatus> {
32+
pub fn run_child(
33+
argv: &[OsString],
34+
creds: &Credentials,
35+
env: &HashMap<String, String>,
36+
) -> Result<ExitStatus> {
3237
// Don't use tokio::process::Command because it wants to reap the process.
3338
// However we need to run waitpid() ourselves to reap the zombies and it'll
3439
// end up picking up the spawned child as well.
3540
let child = Command::new(&argv[0])
3641
.args(&argv[1..])
42+
.envs(env)
3743
.uid(creds.uid)
3844
.gid(creds.gid)
3945
.process_group(0)
@@ -46,8 +52,12 @@ pub fn run_child(argv: &[OsString], creds: &Credentials) -> Result<ExitStatus> {
4652
}
4753

4854
// runs the child and reaps all of its children as well
49-
pub fn start_child(argv: Vec<OsString>, creds: Credentials) -> JoinHandle<Result<ExitStatus>> {
50-
tokio::task::spawn_blocking(move || run_child(&argv, &creds))
55+
pub fn start_child(
56+
argv: Vec<OsString>,
57+
creds: Credentials,
58+
env: HashMap<String, String>,
59+
) -> JoinHandle<Result<ExitStatus>> {
60+
tokio::task::spawn_blocking(move || run_child(&argv, &creds, &env))
5161
}
5262

5363
// Reap processes until a process with sentinel pid exits.

enclaver/src/bin/odyn/main.rs

Lines changed: 72 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@ pub mod ingress;
99
pub mod kms_proxy;
1010
pub mod launcher;
1111

12-
use anyhow::Result;
12+
use anyhow::{anyhow, Context, Result};
1313
use clap::Parser;
14-
use log::{error, info};
14+
use log::{error, info, warn};
15+
use std::collections::HashMap;
1516
use std::ffi::OsString;
1617
use std::sync::Arc;
18+
use tokio::time;
1719

18-
use enclaver::constants::{APP_LOG_PORT, STATUS_PORT};
20+
use enclaver::constants::{APP_LOG_PORT, ENV_SYNC_PORT, STATUS_PORT};
1921
use enclaver::nsm::Nsm;
2022

2123
use api::ApiService;
@@ -25,6 +27,8 @@ use egress::EgressService;
2527
use ingress::IngressService;
2628
use kms_proxy::KmsProxyService;
2729

30+
const ENV_SYNC_TIMEOUT: time::Duration = time::Duration::from_secs(10);
31+
2832
#[derive(Parser)]
2933
struct CliArgs {
3034
#[clap(long = "no-bootstrap", action)]
@@ -57,11 +61,12 @@ async fn launch(args: &CliArgs) -> Result<launcher::ExitStatus> {
5761
let ingress = IngressService::start(&config)?;
5862
let kms_proxy = KmsProxyService::start(config.clone(), nsm.clone()).await?;
5963
let api = ApiService::start(&config, nsm.clone()).await?;
64+
let env = sync_environment(&config).await?;
6065

6166
let creds = launcher::Credentials { uid: 0, gid: 0 };
6267

6368
info!("Starting {:?}", args.entrypoint);
64-
let exit_status = launcher::start_child(args.entrypoint.clone(), creds).await??;
69+
let exit_status = launcher::start_child(args.entrypoint.clone(), creds, env).await??;
6570
info!("Entrypoint {}", exit_status);
6671

6772
api.stop().await;
@@ -72,6 +77,69 @@ async fn launch(args: &CliArgs) -> Result<launcher::ExitStatus> {
7277
Ok(exit_status)
7378
}
7479

80+
async fn sync_environment(config: &Configuration) -> Result<HashMap<String, String>> {
81+
use futures::stream::StreamExt;
82+
use tokio::io::AsyncReadExt;
83+
84+
let mut env: HashMap<String, String> = HashMap::new();
85+
86+
if let Some(ref keys) = config.manifest.env {
87+
if !keys.is_empty() {
88+
info!("Starting the environment sync");
89+
90+
let mut incoming = enclaver::vsock::serve(ENV_SYNC_PORT)?;
91+
92+
match time::timeout(ENV_SYNC_TIMEOUT, incoming.next()).await {
93+
Ok(Some(mut sock)) => {
94+
let mut env_buf: Vec<u8> = Vec::new();
95+
let mut read_buf = vec![0u8; 1024];
96+
const ARG_MAX: usize = 128 * 1024;
97+
98+
loop {
99+
match time::timeout(ENV_SYNC_TIMEOUT, sock.read(&mut read_buf)).await {
100+
Ok(Ok(0)) => break,
101+
Ok(Ok(n)) => {
102+
if env_buf.len() + n > ARG_MAX {
103+
return Err(anyhow!("Maximum environment size exceeded"));
104+
}
105+
env_buf.extend_from_slice(&read_buf[..n]);
106+
}
107+
Ok(Err(err)) => {
108+
return Err(anyhow!("Error reading environment: {:#}", err))
109+
}
110+
Err(_) => return Err(anyhow!("Timed out while reading environment")),
111+
}
112+
}
113+
114+
let mut synced_env: HashMap<String, String> = serde_json::from_slice(&env_buf)
115+
.context("Failed to parse the synced environment")?;
116+
117+
for k in keys.iter() {
118+
if let Some(v) = synced_env.remove(k) {
119+
info!("Syncing environment variable: {}", k);
120+
env.insert(k.clone(), v);
121+
}
122+
}
123+
}
124+
Ok(None) => {
125+
return Err(anyhow!(
126+
"Failed to accept environment sync vsock connection"
127+
));
128+
}
129+
Err(_) => {
130+
return Err(anyhow!("Timed out while waiting for environment sync"));
131+
}
132+
}
133+
134+
info!("Environment sync complete");
135+
} else {
136+
warn!("The list of environment keys in the manifest is empty, skipping the sync");
137+
}
138+
}
139+
140+
Ok(env)
141+
}
142+
75143
async fn run(args: &CliArgs) -> Result<()> {
76144
// Start the status and logs listeners ASAP so that if we fail to
77145
// initialize, we can communicate the status and stream the logs

enclaver/src/constants.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ pub const RELEASE_BUNDLE_DIR: &str = "/enclave";
1313
pub const STATUS_PORT: u32 = 17000;
1414
pub const APP_LOG_PORT: u32 = 17001;
1515
pub const HTTP_EGRESS_VSOCK_PORT: u32 = 17002;
16+
pub const ENV_SYNC_PORT: u32 = 17003;
1617

1718
// Default TCP Port that the egress proxy listens on inside the enclave, if not
1819
// specified in the manifest.

enclaver/src/manifest.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ pub struct Manifest {
2323
pub defaults: Option<Defaults>,
2424
pub kms_proxy: Option<KmsProxy>,
2525
pub api: Option<Api>,
26+
pub env: Option<Vec<String>>,
2627
}
2728

2829
#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]

enclaver/src/run.rs

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
use crate::constants::{
2-
APP_LOG_PORT, EIF_FILE_NAME, HTTP_EGRESS_VSOCK_PORT, MANIFEST_FILE_NAME, RELEASE_BUNDLE_DIR,
3-
STATUS_PORT,
2+
APP_LOG_PORT, EIF_FILE_NAME, ENV_SYNC_PORT, HTTP_EGRESS_VSOCK_PORT, MANIFEST_FILE_NAME,
3+
RELEASE_BUNDLE_DIR, STATUS_PORT,
44
};
55
use crate::manifest::{load_manifest, Defaults, Manifest};
66
use crate::utils;
77
use anyhow::{anyhow, Result};
88
use futures_util::stream::StreamExt;
99
use log::{debug, error, info};
1010
use serde::{Deserialize, Serialize};
11+
use std::collections::HashMap;
12+
use std::env;
1113
use std::path::PathBuf;
1214
use std::time::Duration;
1315
use tokio::fs::File;
16+
use tokio::io::AsyncWriteExt;
1417
use tokio_util::codec::{FramedRead, LinesCodec};
1518
use tokio_util::sync::CancellationToken;
1619
use tokio_vsock::VsockStream;
@@ -19,6 +22,7 @@ use crate::nitro_cli::{EnclaveInfo, NitroCLI, RunEnclaveArgs};
1922
use crate::proxy::egress_http::HostHttpProxy;
2023
use crate::proxy::ingress::HostProxy;
2124

25+
const ENV_VSOCK_RETRY_INTERVAL: Duration = Duration::from_millis(250);
2226
const LOG_VSOCK_RETRY_INTERVAL: Duration = Duration::from_millis(250);
2327
const STATUS_VSOCK_RETRY_INTERVAL: Duration = Duration::from_millis(250);
2428
const STATUS_VSOCK_RETRY_LIMIT: i32 = 100;
@@ -147,6 +151,8 @@ impl Enclave {
147151
self.attach_debug_console(&enclave_info.id).await?;
148152
}
149153

154+
self.start_env_sync(enclave_info.cid)?;
155+
150156
self.start_odyn_log_stream(enclave_info.cid)?;
151157

152158
self.start_ingress_proxies(enclave_info.cid).await?;
@@ -216,6 +222,38 @@ impl Enclave {
216222
Ok(())
217223
}
218224

225+
fn start_env_sync(&mut self, cid: u32) -> Result<()> {
226+
if let Some(ref keys) = self.manifest.env {
227+
if !keys.is_empty() {
228+
let vars = keys
229+
.iter()
230+
.filter_map(|k| env::var(k).ok().map(|v| (k.clone(), v)))
231+
.collect::<HashMap<_, _>>();
232+
let env = serde_json::to_string(&vars)?;
233+
234+
self.tasks
235+
.push(utils::spawn!("sync environment", async move {
236+
info!("waiting for enclave to boot to sync environment");
237+
let mut conn = loop {
238+
match VsockStream::connect(cid, ENV_SYNC_PORT).await {
239+
Ok(conn) => break conn,
240+
// TODO: improve the polling frequency / backoff / timeout
241+
Err(_) => tokio::time::sleep(ENV_VSOCK_RETRY_INTERVAL).await,
242+
}
243+
};
244+
245+
info!("connected to enclave, starting environment sync");
246+
if let Err(err) = conn.write_all(env.as_bytes()).await {
247+
error!("error sending environment to enclave: {err}");
248+
}
249+
info!("environment sync complete");
250+
})?);
251+
}
252+
}
253+
254+
Ok(())
255+
}
256+
219257
fn start_odyn_log_stream(&mut self, cid: u32) -> Result<()> {
220258
self.tasks
221259
.push(utils::spawn!("odyn log stream", async move {

0 commit comments

Comments
 (0)