Skip to content

Added comments based on my current understanding of the orchestration code for obtaining keytabs from AD #602

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
45 changes: 40 additions & 5 deletions rust/krb5-provision-keytab/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,25 +71,60 @@ pub async fn provision_keytab(krb5_config_path: &Path, req: &Request) -> Result<
let req_str = serde_json::to_vec(&req).context(SerializeRequestSnafu)?;

let mut child = Command::new("stackable-krb5-provision-keytab")
// make sure the process is killed if we error out of this fn somewhere due to
// an error when writing to stdin or getting stdout
// Usually we'd expect the process to terminate on its own, this is a fail safe to ensure
// it gets killed in case it hangs for some reason.
.kill_on_drop(true)
.env("KRB5_CONFIG", krb5_config_path)
// ldap3 uses the default client keytab to authenticate to the LDAP server
.env("KRB5_CLIENT_KTNAME", &req.admin_keytab_path)
// avoid leaking credentials between secret volumes/secretclasses
// avoid leaking credentials between secret volumes/secretclasses by only storing the
// TGT that is obtained for the operation in the memory of the short lives process
// spawned by `Command::new` above - this way it'll be wiped from memory once this exits
// With any shared or persistent ticket cache this might stick around and potentially be
// reused by other volumes (which could cause privilege escalations and similar fun issues)
.env("KRB5CCNAME", "MEMORY:")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.context(SpawnProvisionerSnafu)?;
let mut stdin = child.stdin.take().unwrap();

// Get a `ChildStdin` object for the spawned process and write the serialized request
// for a Principal into it in order for the child process to deserialize it and
// process the request
let mut stdin = child
.stdin
.take()
.expect("Failed to read from stdin of stackable-krb5-provision-keytab script! ");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a bit more accurate, but not sure

Suggested change
.expect("Failed to read from stdin of stackable-krb5-provision-keytab script! ");
.expect("Failed to read from stdin of stackable-krb5-provision-keytab command! ");

Copy link
Contributor

@nightkr nightkr Jul 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I must've missed that.. it's not even about reading, it's about taking ownership of the pipe. If it fails then you either didn't set up the pipe correctly (stdin(Stdio::piped())), or already took the pipe elsewhere.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just saw that you provided more details in #602 (comment), thanks!

stdin.write_all(&req_str).await.context(WriteRequestSnafu)?;
stdin.flush().await.context(WriteRequestSnafu)?;
drop(stdin);

// Wait for the process to finish and capture output
// This will always return Ok(...) regardless of exit code or output of the child process
// Failure here means that something went wrong with connecting to the process or obtaining
// exit code or output
let output = child
.wait_with_output()
.await
.context(WaitProvisionerSnafu)?;
serde_json::from_slice::<Result<Response, String>>(&output.stdout)
.context(DeserializeResponseSnafu)?
.map_err(|msg| Error::RunProvisioner { msg })

// Check if the spawned command returned 0 as return code
if output.status.success() {
// Check for success of the operation by deserializing stdout of the process to a `Response`
// struct - since `Response` is an empty struct with no fields this effectively means that
// any output will fail to deserialize and cause an `Error::RunProvisioner` to be propagated
// with the output of the child process
serde_json::from_slice::<Result<Response, String>>(&output.stdout)
.context(DeserializeResponseSnafu)?
.map_err(|msg| Error::RunProvisioner { msg })
} else {
Err(Error::RunProvisioner {
msg: format!(
"Got non zero return code from stackable-krb5-provision-keytab: [{:?}]",
output.status.code()
),
})
}
}