Skip to content
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

Add a new RPC ConnectWithCreds to allow gofer to connect to a unix domain socket with application's credentials #11291

Merged
merged 1 commit into from
Jan 7, 2025

Conversation

xianzhe-databricks
Copy link
Contributor

@xianzhe-databricks xianzhe-databricks commented Dec 13, 2024

Dear gvisor developers,

Thank you very much for maintaining / developing gvisor!

Motivation

We had a use case (which I believe is a wide use case) that the sandboxes send requests over a unix domain socket on host, which is mapped to the container's file system and listened to by a server on the local host.

The sandboxed application is started with a prescribed uid. To authenticate the request, the server verifies the request's uid.

However, as the gofer process (which usually runs as root) executes connect(unix_domain_socket) call on behalf of the sandbox, the server always sees a uid 0. Hence the server cannot authenticate the UDS requests coming from the sandbox.

Proposal

I propose to Add a new RPC ConnectWithCreds to allow gofer to connect to a unix domain socket with application's credentials. On that gofer server thread, the euid/egid are temporarily changed to application's uid/gid and restored after the connect(2) call.

Questions

What do you think of this change? Is there any security/ functionality concern? Thank you so much for your feedback!

Copy link

google-cla bot commented Dec 13, 2024

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@xianzhe-databricks
Copy link
Contributor Author

xianzhe-databricks commented Dec 13, 2024

I will prepare some sample code to reproduce this issue. Databricks should already have the CLA with Google. I will ask our PoC to include me in the group of contributors.

@ayushr2
Copy link
Collaborator

ayushr2 commented Dec 13, 2024

Is the server using SO_PEERCRED? We had a similar issue in gVisor where our control server was using SO_PEERCRED to authenticate requests and due to the different user namespace mappings the sandbox process was running under, the check always seemed to pass. We got rid of the SO_PEERCRED check and started only relying on filesystem permission check; see 36b9b19 and d81768d.

I understand that your situation is different, but could you use the same logic in your case? To connect(2), the caller needs write permissions on the UDS. If the UDS that you share with the sandbox is owned by UID=10 and the app is running with UID=8, then the connect(2) attempt from the sandbox should fail.

cc @avagin about implications of running the gofer with euid=spec.Process.User.UID.

@xianzhe-databricks
Copy link
Contributor Author

xianzhe-databricks commented Dec 13, 2024

Is the server using SO_PEERCRED? We had a similar issue in gVisor where our control server was using SO_PEERCRED to authenticate requests and due to the different user namespace mappings the sandbox process was running under, the check always seemed to pass. We got rid of the SO_PEERCRED check and started only relying on filesystem permission check; see 36b9b19 and d81768d.

I understand that your situation is different, but could you use the same logic in your case? To connect(2), the caller needs write permissions on the UDS. If the UDS that you share with the sandbox is owned by UID=10 and the app is running with UID=8, then the connect(2) attempt from the sandbox should fail.

cc @avagin about implications of running the gofer with euid=spec.Process.User.UID.

Thanks for the fast reply! Yes the server uses SO_PEERCRED.

We cannot use file permission because, there is only one unix domain socket file on the host, which the server listens to. This unix domain socket file is mapped to every sandbox' filesystem, i.e. every sandbox should have the capability to talk to the server over this unix domain socket. The server utilizes the uid to prevent the case that one sandbox impersonates another sandbox when talking to the server.

@EtiennePerot
Copy link
Contributor

Would this problem be solved by running sandboxes in --rootless mode, each with a distinct user?

@xianzhe-databricks
Copy link
Contributor Author

xianzhe-databricks commented Dec 13, 2024

Would this problem be solved by running sandboxes in --rootless mode, each with a distinct user?

Yeah we considered this. However this comes with the cost of much weakened security guarantees, for example the sandbox's isolated network stack cannot be used. Directfs can also not be used.

I think in general the sandbox's host-filesystem-access shouldn't be performed with a root entity. This is already a security concern. What do you think?

@ayushr2
Copy link
Collaborator

ayushr2 commented Dec 13, 2024

I think in general the sandbox's host-filesystem-access shouldn't be performed with a root entity. This is already a security concern. What do you think?

The gofer is running in a pivot_root(2) which only has the container filesystem (the rootfs + any bind mounts) mounted in it. But yes it can create the SO_PEERCRED kind of issues you have seen. Note that the application in the container may not always be running with the UID from spec.Process.User.UID. The application can change UID/GID and we need to gofer to replicate that behavior. That is why we pass UID/GID as part of RPC messages that create files. For example, see Mkdir() implementation:

if err := unix.Fchownat(childDirFd, "", int(uid), int(gid), unix.AT_EMPTY_PATH|unix.AT_SYMLINK_NOFOLLOW); err != nil {
unix.Close(childDirFd)
return nil, linux.Statx{}, err
}

So the gofer runs as root and changes UID/GID for created files based on the application thread's UID/GID. I think running as root is also needed for the special capabilities that the gofer needs to run with:

var caps = []string{
"CAP_CHOWN",
"CAP_DAC_OVERRIDE",
"CAP_DAC_READ_SEARCH",
"CAP_FOWNER",
"CAP_FSETID",
"CAP_SYS_CHROOT",
}
// goferCaps is the minimal set of capabilities needed by the Gofer to operate
// on files.
var goferCaps = &specs.LinuxCapabilities{
Bounding: caps,
Effective: caps,
Permitted: caps,
}

I think we can make an exception for connect(2) implementation and temporarily set euid to the caller's euid while making the connect(2) host syscall. But I think we will need to add another lisafs RPC because the current Connect implementation doesn't have information about user UID/GID:

gvisor/pkg/lisafs/message.go

Lines 1296 to 1306 in bd0cbf8

// ConnectReq is used to make a Connect request.
//
// +marshal boundCheck
type ConnectReq struct {
FD FDID
// SockType is used to specify the socket type to connect to. As a special
// case, SockType = 0 means that the socket type does not matter and the
// requester will accept any socket type.
SockType uint32
_ uint32 // Need to make struct packed.
}

@xianzhe-databricks
Copy link
Contributor Author

xianzhe-databricks commented Dec 16, 2024

@ayushr2 Thank you so much for your detailed reply!

The gofer is running in a pivot_root(2) which only has the container filesystem (the rootfs + any bind mounts) mounted in it.

However the bind mounts represent files on host filesystem. The sandbox's file access to bind mounts is performed with root privilege due to gofer. Would this already be a security concern?

Note that the application in the container may not always be running with the UID from spec.Process.User.UID. The application can change UID/GID and we need to gofer to replicate that behavior.

The application usually runs as non-root. How is it possible/ What are the cases for a non-root to change its euid at runtime (besides the corner case of changing to its ruid or saved uid)?

I think we can make an exception for connect(2) implementation and temporarily set euid to the caller's euid while making the connect(2) host syscall.

I have two concerns here:

  1. Would this create a race condition? I.e. there might be concurrent RPC call which requires a euid 0, while this temporary change takes place.
  2. We cannot rely on the uid/gid information passed from RPC message here, as what we did for the mkdir implementation. The execution context within the sandbox is untrusted and can in theory pass any uid/gid pair to the RPC call as it wishes. It would allow connecting to the unix domain socket with arbitrary credential as the attacker likes.

@xianzhe-databricks
Copy link
Contributor Author

xianzhe-databricks commented Dec 16, 2024

We somewhat urgently rely on solving this issue. May I be able to expect an answer from you on this? @ayushr2 @avagin Thank you so much!!

@xianzhe-databricks
Copy link
Contributor Author

xianzhe-databricks commented Dec 16, 2024

Also a follow-up question:

I think running as root is also needed for the special capabilities that the gofer needs to run with

Then what happens with the -rootless mode? My previous assumption was that gofer only needs root privilege during the initialization phase (to set up security guardrails). When the gofer server is started, it could run without root (since -rootless mode anyway runs gofer without root).

Also aren't linux capabilities designed to grant non-root processes the fine-grained root-like privileged actions? I think just because we grant gofer these capabilities, gofer can run without root

var caps = []string{
"CAP_CHOWN",
"CAP_DAC_OVERRIDE",
"CAP_DAC_READ_SEARCH",
"CAP_FOWNER",
"CAP_FSETID",
"CAP_SYS_CHROOT",
}
// goferCaps is the minimal set of capabilities needed by the Gofer to operate
// on files.
var goferCaps = &specs.LinuxCapabilities{
Bounding: caps,
Effective: caps,
Permitted: caps,
}

I also verified that the unix.Fchownat succeeds even with Gofer's uid changed to spec.Process.User.UID. I believe it is just because gofer is granted the capabilities CAP_CHOWN and CAP_FOWNER. Note that with directfs enabled, the Mkdir system call is even not routed to gofer.

runsc/cmd/gofer.go Outdated Show resolved Hide resolved
runsc/cmd/gofer.go Outdated Show resolved Hide resolved
@ayushr2
Copy link
Collaborator

ayushr2 commented Dec 18, 2024

However the bind mounts represent files on host filesystem. The sandbox's file access to bind mounts is performed with root privilege due to gofer. Would this already be a security concern?

Note that the sentry does all necessary filesystem permission checks and only makes authenticated RPCs to the gofer, after which the gofer takes actions as root to fulfill the filesystem request. The attacker needs to compromise the sentry to bypass sentry checks and make arbitrary RPCs to the gofer. Even if that does happen, then our second layer of defense here is the gofer. It limits the attacker to just the filesystem that has been exposed to the container via bind mounts and prevents the attacker from accessing the host filesystem. That being said, if we can actually run the gofer in non-root user, it would be nice.

How is it possible/ What are the cases for a non-root to change its euid at runtime (besides the corner case of changing to its ruid or saved uid)?

I believe it is possible that the user can call runsc exec which can start a new process with another uid/gid.

Would this create a race condition? I.e. there might be concurrent RPC call which requires a euid 0, while this temporary change takes place.

setuid/setgid only changes the users/groups for the current system thread. So we need to use runtime.LockOSThread() and runtime.UnlockOSThread() during the temporary change in UID if we were to do this.

@ayushr2
Copy link
Collaborator

ayushr2 commented Dec 18, 2024

Please squash all your commits into 1, because all commits from this branch will be applied directly to master if we were to merge this.

pkg/lisafs/connection.go Outdated Show resolved Hide resolved
runsc/fsgofer/filter/config.go Outdated Show resolved Hide resolved
runsc/cmd/gofer.go Outdated Show resolved Hide resolved
@xianzhe-databricks
Copy link
Contributor Author

Please squash all your commits into 1, because all commits from this branch will be applied directly to master if we were to merge this.

Thanks for reminding this! I will keep the commit history as-is for the moment, and when we are ready to merge, I squash all the commits and do a force-push.

@xianzhe-databricks xianzhe-databricks changed the title A proposal to fix an issue to authenticate the unix domain socket request from a sandbox Add a new RPC ConnectWithCreds to allow gofer to connect to a unix domain socket with application's credentials Dec 20, 2024
@xianzhe-databricks
Copy link
Contributor Author

@ayushr2 could you approve of the github workflows to see how tests are going? Thank you so much!

pkg/lisafs/message.go Outdated Show resolved Hide resolved
pkg/sentry/BUILD Outdated Show resolved Hide resolved
pkg/lisafs/BUILD Outdated Show resolved Hide resolved
runsc/fsgofer/lisafs.go Outdated Show resolved Hide resolved
runsc/fsgofer/lisafs.go Outdated Show resolved Hide resolved
pkg/sentry/socket/unix/transport/connectionless.go Outdated Show resolved Hide resolved
pkg/sentry/socket/unix/unix.go Outdated Show resolved Hide resolved
@xianzhe-databricks
Copy link
Contributor Author

xianzhe-databricks commented Jan 2, 2025

@ayushr2 Happy new year and thank you so much for the guidance along the way. I did a final check and made sure the unit tests and integration tests passed.

The fix is needed by us urgently. I really appreciate it if we could make the fix included in next week's release. Could you let me know what else is left for the merge (besides squashing the commits)?

Copy link
Collaborator

@ayushr2 ayushr2 left a comment

Choose a reason for hiding this comment

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

Just nits, mostly LGTM

pkg/lisafs/message.go Outdated Show resolved Hide resolved
pkg/sentry/kernel/auth/context.go Outdated Show resolved Hide resolved
runsc/fsgofer/lisafs.go Show resolved Hide resolved
test/e2e/integration_runtime_test.go Outdated Show resolved Hide resolved
test/e2e/integration_runtime_test.go Outdated Show resolved Hide resolved
test/e2e/integration_runtime_test.go Outdated Show resolved Hide resolved
runsc/fsgofer/lisafs.go Show resolved Hide resolved
pkg/lisafs/client_file.go Outdated Show resolved Hide resolved
@ayushr2
Copy link
Collaborator

ayushr2 commented Jan 3, 2025

Its probably time to squash all commits BTW.

@xianzhe-databricks
Copy link
Contributor Author

Its probably time to squash all commits BTW.

Thanks. The commits were squashed.

@xianzhe-databricks
Copy link
Contributor Author

Just did another final check. The PR can be merged from our side. Thank you so much!

@xianzhe-databricks
Copy link
Contributor Author

xianzhe-databricks commented Jan 6, 2025

@ayushr2 shall we create a copybara PR for this one? Will we able to include it in this week's release? Thanks!

@ayushr2
Copy link
Collaborator

ayushr2 commented Jan 6, 2025

Its undergoing internal review and needs approval from someone other than me since I approved it here. I don't think we will be able to make it into this week's release.

@xianzhe-databricks
Copy link
Contributor Author

Its undergoing internal review and needs approval from someone other than me since I approved it here. I don't think we will be able to make it into this week's release.

I see. Thanks for informing me!

copybara-service bot pushed a commit that referenced this pull request Jan 6, 2025
…domain socket with application's credentials

Dear gvisor developers,

Thank you very much for maintaining / developing gvisor!

## Motivation
We had a use case (which I believe is a wide use case) that the sandboxes send requests over a unix domain socket on host, which is mapped to the container's file system and listened to by a server on the local host.

The sandboxed application is started with a prescribed uid. To authenticate the request, the server verifies the request's uid.

However, as the gofer process (which usually runs as root) executes [connect(unix_domain_socket) call](https://github.com/google/gvisor/blob/bd0cbf807169db29837d238209c02c816f3c8dbf/runsc/fsgofer/lisafs.go#L819) on behalf of the sandbox, the server always sees a uid 0. Hence the server cannot authenticate the UDS requests coming from the sandbox.

## Proposal
I propose to Add a new RPC `ConnectWithCreds` to allow gofer to connect to a unix domain socket with application's credentials. On that gofer server thread, the euid/egid are temporarily changed to application's uid/gid and restored after the `connect(2)` call.

## Questions
What do you think of this change? Is there any security/ functionality concern? Thank you so much for your feedback!

FUTURE_COPYBARA_INTEGRATE_REVIEW=#11291 from xianzhe-databricks:fix-uds-auth c4f686f
PiperOrigin-RevId: 712489714
runsc/fsgofer/lisafs.go Show resolved Hide resolved
runsc/fsgofer/lisafs.go Show resolved Hide resolved
copybara-service bot pushed a commit that referenced this pull request Jan 6, 2025
…domain socket with application's credentials

Dear gvisor developers,

Thank you very much for maintaining / developing gvisor!

## Motivation
We had a use case (which I believe is a wide use case) that the sandboxes send requests over a unix domain socket on host, which is mapped to the container's file system and listened to by a server on the local host.

The sandboxed application is started with a prescribed uid. To authenticate the request, the server verifies the request's uid.

However, as the gofer process (which usually runs as root) executes [connect(unix_domain_socket) call](https://github.com/google/gvisor/blob/bd0cbf807169db29837d238209c02c816f3c8dbf/runsc/fsgofer/lisafs.go#L819) on behalf of the sandbox, the server always sees a uid 0. Hence the server cannot authenticate the UDS requests coming from the sandbox.

## Proposal
I propose to Add a new RPC `ConnectWithCreds` to allow gofer to connect to a unix domain socket with application's credentials. On that gofer server thread, the euid/egid are temporarily changed to application's uid/gid and restored after the `connect(2)` call.

## Questions
What do you think of this change? Is there any security/ functionality concern? Thank you so much for your feedback!

FUTURE_COPYBARA_INTEGRATE_REVIEW=#11291 from xianzhe-databricks:fix-uds-auth c4f686f
PiperOrigin-RevId: 712489714
copybara-service bot pushed a commit that referenced this pull request Jan 7, 2025
…domain socket with application's credentials

Dear gvisor developers,

Thank you very much for maintaining / developing gvisor!

## Motivation
We had a use case (which I believe is a wide use case) that the sandboxes send requests over a unix domain socket on host, which is mapped to the container's file system and listened to by a server on the local host.

The sandboxed application is started with a prescribed uid. To authenticate the request, the server verifies the request's uid.

However, as the gofer process (which usually runs as root) executes [connect(unix_domain_socket) call](https://github.com/google/gvisor/blob/bd0cbf807169db29837d238209c02c816f3c8dbf/runsc/fsgofer/lisafs.go#L819) on behalf of the sandbox, the server always sees a uid 0. Hence the server cannot authenticate the UDS requests coming from the sandbox.

## Proposal
I propose to Add a new RPC `ConnectWithCreds` to allow gofer to connect to a unix domain socket with application's credentials. On that gofer server thread, the euid/egid are temporarily changed to application's uid/gid and restored after the `connect(2)` call.

## Questions
What do you think of this change? Is there any security/ functionality concern? Thank you so much for your feedback!

FUTURE_COPYBARA_INTEGRATE_REVIEW=#11291 from xianzhe-databricks:fix-uds-auth c4f686f
PiperOrigin-RevId: 712489714
@copybara-service copybara-service bot merged commit 7aa4c49 into google:master Jan 7, 2025
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants