-
Notifications
You must be signed in to change notification settings - Fork 787
feat(sandbox): proxy-side AWS SigV4 credential signing for CONNECT tunnels #1638
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -377,6 +377,9 @@ where | |
| generation_guard, | ||
| websocket_extensions: WebSocketExtensionMode::Preserve, | ||
| request_body_credential_rewrite: false, | ||
| credential_signing: crate::l7::CredentialSigning::None, | ||
| signing_service: "", | ||
| host: "", | ||
| }, | ||
| ) | ||
| .await | ||
|
|
@@ -395,6 +398,9 @@ pub(crate) struct RelayRequestOptions<'a> { | |
| pub(crate) generation_guard: Option<&'a PolicyGenerationGuard>, | ||
| pub(crate) websocket_extensions: WebSocketExtensionMode, | ||
| pub(crate) request_body_credential_rewrite: bool, | ||
| pub(crate) credential_signing: crate::l7::CredentialSigning, | ||
| pub(crate) signing_service: &'a str, | ||
| pub(crate) host: &'a str, | ||
| } | ||
|
|
||
| pub(crate) async fn relay_http_request_with_options_guarded<C, U>( | ||
|
|
@@ -421,8 +427,19 @@ where | |
| parse_websocket_upgrade_request(&req.raw_header[..header_end])? | ||
| }; | ||
|
|
||
| // When SigV4 signing is configured, strip AWS auth headers before credential | ||
| // rewriting so the fail-closed placeholder scan doesn't reject the SigV4 | ||
| // Authorization header (which embeds placeholder strings). | ||
| let raw_for_rewrite; | ||
| let header_source = if options.credential_signing == crate::l7::CredentialSigning::SigV4 { | ||
| raw_for_rewrite = crate::sigv4::strip_aws_headers(&req.raw_header[..header_end]); | ||
| &raw_for_rewrite[..] | ||
| } else { | ||
| &req.raw_header[..header_end] | ||
| }; | ||
|
|
||
| let (header_bytes, expected_websocket_extension) = rewrite_websocket_extensions_for_mode( | ||
| &req.raw_header[..header_end], | ||
| header_source, | ||
| options.websocket_extensions, | ||
| websocket_request.is_some(), | ||
| )?; | ||
|
|
@@ -442,7 +459,82 @@ where | |
| guard.ensure_current()?; | ||
| } | ||
|
|
||
| if options.request_body_credential_rewrite { | ||
| // Apply SigV4 signing if configured. We need the full request (headers + body) | ||
| // to compute the signature, so for SigV4 we always buffer the body first. | ||
| if options.credential_signing == crate::l7::CredentialSigning::SigV4 { | ||
| if let Some(resolver) = options.resolver { | ||
| let access_key_placeholder = | ||
| crate::secrets::placeholder_for_env_key("AWS_ACCESS_KEY_ID"); | ||
| let secret_key_placeholder = | ||
| crate::secrets::placeholder_for_env_key("AWS_SECRET_ACCESS_KEY"); | ||
| let session_token_placeholder = | ||
| crate::secrets::placeholder_for_env_key("AWS_SESSION_TOKEN"); | ||
|
|
||
| match ( | ||
| resolver.resolve_placeholder(&access_key_placeholder), | ||
| resolver.resolve_placeholder(&secret_key_placeholder), | ||
| ) { | ||
| (Some(access_key), Some(secret_key)) => { | ||
| let session_token = resolver.resolve_placeholder(&session_token_placeholder); | ||
| let region = crate::sigv4::extract_aws_region(options.host) | ||
| .unwrap_or_else(|| "us-east-1".to_string()); | ||
| let service = &options.signing_service; | ||
| if service.is_empty() { | ||
| return Err(miette!( | ||
| "SigV4 signing configured but signing_service not set in policy" | ||
| )); | ||
|
jhjaggars marked this conversation as resolved.
|
||
| } | ||
| debug!( | ||
| host = %options.host, | ||
| region = %region, | ||
| service = %service, | ||
| "applying SigV4 signing to CONNECT tunnel request" | ||
| ); | ||
|
|
||
| // Collect body from overflow + stream | ||
| let overflow = &req.raw_header[header_end..]; | ||
| let mut full_request = rewrite_result.rewritten.clone(); | ||
| full_request.extend_from_slice(overflow); | ||
| // Read remaining body based on content-length | ||
| if let BodyLength::ContentLength(body_len) = parse_body_length(header_str)? { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One issue that we can address in a follow-up is that this doesn't support chunked transfer (no content length for the whole body). When I was trying to test S3 access using the python boto library, it was doing chunked transfer. I started working on this in my branch, so maybe I'll get it worked out, but it's worth noting the limitation I think.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I gave this a shot, but it turned out to be a little harder than I thought. I think most aws services support skipping body signatures which is probably what we should do, but I think there are some edge cases that make it a little tricky.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, I didn't get it working, either. Skipping sounds good if supported! |
||
| if body_len > MAX_REWRITE_BODY_BYTES as u64 { | ||
| return Err(miette!( | ||
| "SigV4 signing buffers at most {MAX_REWRITE_BODY_BYTES} bytes" | ||
| )); | ||
| } | ||
| let already_have = overflow.len() as u64; | ||
| if body_len > already_have { | ||
| let remaining = | ||
| usize::try_from(body_len - already_have).unwrap_or(usize::MAX); | ||
| let mut body_buf = vec![0u8; remaining]; | ||
| client.read_exact(&mut body_buf).await.into_diagnostic()?; | ||
| full_request.extend_from_slice(&body_buf); | ||
| } | ||
| } | ||
|
|
||
| let signed = crate::sigv4::apply_sigv4_to_request( | ||
| &full_request, | ||
| options.host, | ||
| ®ion, | ||
| service, | ||
| access_key, | ||
| secret_key, | ||
| session_token, | ||
| )?; | ||
| upstream.write_all(&signed).await.into_diagnostic()?; | ||
| } | ||
| _ => { | ||
| return Err(miette!( | ||
| "SigV4 signing configured but AWS credentials not found in provider" | ||
| )); | ||
| } | ||
| } | ||
| } else { | ||
| return Err(miette!( | ||
| "SigV4 signing configured but no secret resolver available" | ||
| )); | ||
| } | ||
| } else if options.request_body_credential_rewrite { | ||
| let body = collect_and_rewrite_request_body( | ||
| req, | ||
| client, | ||
|
|
||
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.
Are there practical extensions to the values for these fields? Is there ever a non-AWS use case? Just trying to understand how AWS-specific these settings are vs the names of the settings that imply they could map to other service provider use cases.
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.
These feel pretty specific to AWS. It's awkward, but I didn't see an obviously better way to have a special case like this today.
credential_signingmight be used in other services, but thesigning_servicealmost certainly wouldn't.Uh oh!
There was an error while loading. Please reload this page.
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 could imagine a cleaner way of expressing these things aligning with your issue here: #896. It does seem like a middleware pattern would fit nicely as well.