-
Notifications
You must be signed in to change notification settings - Fork 1k
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
Support Confidential Sealed Secrets (as env vars) #9719
base: main
Are you sure you want to change the base?
Conversation
Hey @fitzthum! First of all, thanks for taking over this work and opening the PR, very much appreciated. I see this touches mainly 2 parts of the code:
When we have something enabled / disabled only during build time on the agent side, it basically means that in order to properly test it we'd have to create a new agent build for that specific case, and then add this new specific agent into the rootfs / initrd, which is a pain as it leads to a way too huge amount of things to test. With this in mind, I´d like to ask you, is there the possibility to have this still as an option, still defaulting to not using it in the agent's Makefile, but enabled by default in the agent build (here:
|
@fidencio Currently the sealed-secret stuff in the agent is behind the You are suggesting setting |
I'd prefer that. I understand that it'll increase the binary size, but what we ship here is an example of what people should use, and I sincerely hope people end up building their own content when they decide to get kata-containers into their production environment. With that said, as long as we leave a way for folks to build the agent without sealed secrets support, we're good. That's better than having to do deal with different agent builds. |
ociMounts := spec.Mounts | ||
|
||
for index, m := range ociMounts { | ||
if guestMount, ok := guestMounts[m.Destination]; ok { | ||
k.Logger().Debugf("Replacing OCI mount (%s) source %s with %s", m.Destination, m.Source, guestMount.Source) | ||
if sealedSecretEnabled { | ||
if strings.Contains(m.Source, "kubernetes.io~secret") { | ||
ociMounts[index].Destination = "/sealed" + m.Destination |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure this approach will allow sealed secrets and non-sealed secrets to coexist. Any ideas how to improve? cc: @Xynnn007 maybe
Ideally I'd like to remove the shim support entirely.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not expert upon this. But at the firt glance, this logic will add all secrets (sealed/unsealed) with "/sealed" prefix. Togetherly, at kata-agent side all prefixs will be striped and put all files under /run/secrets/
. Whether to unseal the file is determined by the content format of the secret file. Thus I am afraid we might not need both add-prefix on runtime side and delete-prefix part on agent side.
I am not sure, we might need to do some test for this. With both sealed and non-sealed secret binding to a container and check if it works.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah i suspect there are a couple ways to get rid of the runtime stuff.
Fixed up static checks. I think I will wait for #9749 before reshuffling how the feature is enabled. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are some comments. Most are about the code architecture.
src/agent/src/cdh.rs
Outdated
let sealed_secret = sealed_secret | ||
.strip_prefix("sealed.") | ||
.ok_or(anyhow!("secret prefix not found"))?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In current impl of CDH we delayed the strip in CDH. Thus we do not need to .strip_prefix("sealed.")
here. Just put it into CDH's API.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I removed this and bumped guest-components. Hopefully this won't cause any other issues.
src/agent/src/cdh.rs
Outdated
pub async fn unseal_env(&self, env: &str) -> Result<String> { | ||
let (key, value) = env.split_once('=').unwrap(); | ||
if value.starts_with("sealed.") { | ||
let unsealed_value = self.unseal_secret_async(value).await?; | ||
let unsealed_env = format!("{}={}", key, std::str::from_utf8(&unsealed_value).unwrap()); | ||
|
||
return Ok(unsealed_env); | ||
} | ||
Ok((*env.to_owned()).to_string()) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we do not need the unseal_env()
func. Just do some condition judgement in outer rpc.rs
and if sealed secret are detected (with sealed.
prefix), unseal_secret_async()
will then be called.
btw, let's try to avoid unwrap()
s
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am a bit split on this. I would prefer to keep the handling of the env outside of cdh.rs
, but I would also like to hide as much complexity as possible from rpc.rs
. Maybe other reviewers have a viewpoint on this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Brainstorming: I read this comment after I began to review the code. I confess that I was wondering if those unseal methods wouldn't fit better somewhere else. Sealed secrets seems to not belong to the "cdh" concept, but it seems rather a simple user of cdh (to fetch the secret for unsealing the secret).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah cdh.rs currently contains the direct cdh code (which is very minimal) and also helper functions for some of the operations that use the cdh. I think this is reasonable but I am open to either renaming the file to be more clear or moving some of the helper code somewhere else if anyone has any idea where would be good.
src/agent/src/rpc.rs
Outdated
#[cfg(feature = "sealed-secret")] | ||
cdh_client: Option<CDHClient>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could have a OnceCell<CDHClient>
inside cdh.rs
and only be used in that mod file. And only expose unseal_secret_async()
to higher level. This would help higher callers to ignore the underlying details, thus more modularization.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a good suggestion, but I have refactored the setup of the client to work a bit differently. Now it is instantiated based on what is specified in the new gc_procs
setting. Thus, I am continuing to expose the entire cdh_client to rpc.rs. I think this will also be helpful when we start to introduce other capabilities that depend on cdh.rs.
src/agent/src/rpc.rs
Outdated
#[cfg(feature = "sealed-secret")] | ||
cdh_client: Option<CDHClient>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could have a OnceCell<CDHClient>
inside cdh.rs
and only be used in that mod file. And only expose unseal_secret_async()
to higher level. This would help higher callers to ignore the underlying details, thus more modularization.
ociMounts := spec.Mounts | ||
|
||
for index, m := range ociMounts { | ||
if guestMount, ok := guestMounts[m.Destination]; ok { | ||
k.Logger().Debugf("Replacing OCI mount (%s) source %s with %s", m.Destination, m.Source, guestMount.Source) | ||
if sealedSecretEnabled { | ||
if strings.Contains(m.Source, "kubernetes.io~secret") { | ||
ociMounts[index].Destination = "/sealed" + m.Destination |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not expert upon this. But at the firt glance, this logic will add all secrets (sealed/unsealed) with "/sealed" prefix. Togetherly, at kata-agent side all prefixs will be striped and put all files under /run/secrets/
. Whether to unseal the file is determined by the content format of the secret file. Thus I am afraid we might not need both add-prefix on runtime side and delete-prefix part on agent side.
I am not sure, we might need to do some test for this. With both sealed and non-sealed secret binding to a container and check if it works.
I have some significant changes planned here so I will make this a draft. |
I have found a few issues with the implementation of sealed secrets as files. I have a plan for fixing this, but I think the smart thing to do is to break this PR apart and start only with secrets in environment variables, which are simpler. To that end, I have dropped a couple commits from this PR. I will re-introduce them in a follow-up. Notice that we don't actually need any shim support for sealed secrets to work with environment variables. I am hoping for the same thing when I do secrets in files. Tomorrow I will finally rework the way the feature is loaded and address other comments. |
e5e99f9
to
d5b6851
Compare
I have reshuffled how this feature is enabled. There is no longer any feature required for sealed-secret support (for the agent or the shim). Instead, it can be enabled at runtime simply be setting |
4468ed5
to
562b08f
Compare
src/agent/src/cdh.rs
Outdated
|
||
pub async fn unseal_env(&self, env: &str) -> Result<String> { | ||
let (key, value) = env.split_once('=').unwrap(); | ||
if value.starts_with("sealed.") { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could make this "sealed." as a constant (eg. SEALED_PREFIX
) and document the expected format of a sealed secret. We also could have a simple parser.
I'm also wondering if we shouldn't sanitize the input to avoid kind of injection attack (really don't know if a attack here is doable).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can't think of any issues with injection, but I'll think about it a bit more.
There is a tool for generating sealed secrets in the guest-components repo.
kubectl create secret generic sealed-secret --from-literal='password=sealed.fakejwsheader.ewogICAgInZlcnNpb24iOiAiMC4xLjAiLAogICAgInR5cGUiOiAidmF1bHQiLAogICAgIm5hbWUiOiAia2JzOi8vL2RlZmF1bHQvc2VhbGVkLXNlY3JldC90ZXN0IiwKICAgICJwcm92aWRlciI6ICJrYnMiLAogICAgInByb3ZpZGVyX3NldHRpbmdzIjoge30sCiAgICAiYW5ub3RhdGlvbnMiOiB7fQp9Cg==.fakesignature' | ||
} | ||
|
||
@test "Unseal Secrets with CDH" { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Idea of new test: mix sealed and non-sealed secrects in a single deployment to check it can handle well (if possible make a unit test instead).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you think we should add a new test for this or just extend the existing one? Can this be a follow-up PR?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the idea of checking this mixed scenario. I think a new test might be clearer
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I ended up adding a not-sealed secret to the existing tests to avoid making a second yaml file.
5c73dc2
to
a966d51
Compare
40a5322
to
71af0e2
Compare
Non-tee tests passing (including with the sealed-secret test). Other tests looking ok too. Good time for some more review. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool. The pr looks good. Only a nit
src/agent/src/main.rs
Outdated
sandbox.clone(), | ||
config.server_addr.as_str(), | ||
init_mode, | ||
gc_enabled, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we could directly pass Some(CDHClient)
as parameter here? which would help to avoid splited condition judgement on different places.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea. I moved the initialization of the CDHClient to init_attestation_components
so we can create the CDHClient right after starting the CDIH.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few comments - generally I think it looks great. Maybe updating the PR name to be clearer that it's just sealed secrets via envs, not files would be helpful?
kubectl create secret generic sealed-secret --from-literal='password=sealed.fakejwsheader.ewogICAgInZlcnNpb24iOiAiMC4xLjAiLAogICAgInR5cGUiOiAidmF1bHQiLAogICAgIm5hbWUiOiAia2JzOi8vL2RlZmF1bHQvc2VhbGVkLXNlY3JldC90ZXN0IiwKICAgICJwcm92aWRlciI6ICJrYnMiLAogICAgInByb3ZpZGVyX3NldHRpbmdzIjoge30sCiAgICAiYW5ub3RhdGlvbnMiOiB7fQp9Cg==.fakesignature' | ||
} | ||
|
||
@test "Unseal Secrets with CDH" { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the idea of checking this mixed scenario. I think a new test might be clearer
# Setup k8s secret | ||
kubectl delete secret sealed-secret --ignore-not-found | ||
|
||
# Sealed secret format is defined at: https://github.com/confidential-containers/guest-components/blob/main/confidential-data-hub/docs/SEALED_SECRET.md#vault |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it might be nice for test clarity and customisability (or a real word that means that), to have the secret generated in the test code rather than hard coded, but I'm fine for that to be part of a later PR.
if [ "$SECRET_PASSWORD" == "unsealed_secret" ]; then | ||
echo "unsealed environment as expected" | ||
fi |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Idea: I'm wondering whether having something like echo "Unsealed env secret: $SECRET_PASSWORD"
and then including the secret content in the grep check in the test might be a bit more customisable?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea. Ideally we could generate a random key each time and then package it into a secret. I haven't gone that far, but I've move the check from the yaml file to the bash script.
6e2bd28
to
754e725
Compare
To unseal a secret, the Kata agent will contact the CDH using ttRPC. Add the proto that describes the sealed secret service and messages that will be used. Signed-off-by: Tobin Feldman-Fitzthum <tobin@ibm.com> Signed-off-by: Biao Lu <biao.lu@intel.com>
When sealed-secret is enabled, the Kata Agent intercepts environment variables containing sealed secrets and uses the CDH to unseal the value. Signed-off-by: Tobin Feldman-Fitzthum <tobin@ibm.com> Signed-off-by: Linda Yu <linda.yu@intel.com>
To test unsealing secrets stored in environment variables, we create a simple test server that takes the place of the CDH. We start this server and then use it to unseal a test secret. Signed-off-by: Tobin Feldman-Fitzthum <tobin@ibm.com> Signed-off-by: Linda Yu <linda.yu@intel.com>
The sealed secret test depends on the KBS to provide the unsealed value of a vault secret. This secret is provisioned to an environment variable. Signed-off-by: Tobin Feldman-Fitzthum <tobin@ibm.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think all my suggestions are either for future PRs, or have been address here, so this looks good to me. Thanks!
This PR brings sealed secret functionality to main. This functionality was originally implemented by @LindaYu17 and @Lu-Biao.
I have made some improvements and converted the old sealed secrets test from the defunct CCv0 branch of the tests repo into a test that is similar to the existing CoCo attestation test.
There are still refinements to be made (especially in the last PR). I have done some local testing, but let's see what the CI makes of it.