From 94778981c87df2e07067e2f84c8b2cb9b295f97b Mon Sep 17 00:00:00 2001 From: Renee Chang <136089488+Reneechang17@users.noreply.github.com> Date: Fri, 15 Aug 2025 11:54:56 -0700 Subject: [PATCH] Add SSH auth socket forwarding (#502) Add `--ssh` flag to forward the host's SSH agent socket into the container, so we can use SSH authentication for things like cloning private repos, and also updates the socket path every time the container starts to handle socket path changes like reboot/re-login. Closes #498 --- .../Core/ContainerConfiguration.swift | 8 +++-- Sources/ContainerClient/Flags.swift | 3 ++ Sources/ContainerClient/Utility.swift | 2 ++ .../SandboxService.swift | 32 +++++++++++++++++-- 4 files changed, 41 insertions(+), 4 deletions(-) diff --git a/Sources/ContainerClient/Core/ContainerConfiguration.swift b/Sources/ContainerClient/Core/ContainerConfiguration.swift index b65c358d..29769841 100644 --- a/Sources/ContainerClient/Core/ContainerConfiguration.swift +++ b/Sources/ContainerClient/Core/ContainerConfiguration.swift @@ -40,14 +40,16 @@ public struct ContainerConfiguration: Sendable, Codable { public var rosetta: Bool = false /// Initial or main process of the container. public var initProcess: ProcessConfiguration - /// Platform for the container + /// Platform for the container. public var platform: ContainerizationOCI.Platform = .current /// Resource values for the container. public var resources: Resources = .init() - /// Name of the runtime that supports the container + /// Name of the runtime that supports the container. public var runtimeHandler: String = "container-runtime-linux" /// Configure exposing virtualization support in the container. public var virtualization: Bool = false + /// Enable SSH agent socket forwarding from host to container. + public var ssh: Bool = false enum CodingKeys: String, CodingKey { case id @@ -65,6 +67,7 @@ public struct ContainerConfiguration: Sendable, Codable { case resources case runtimeHandler case virtualization + case ssh } /// Create a configuration from the supplied Decoder, initializing missing @@ -99,6 +102,7 @@ public struct ContainerConfiguration: Sendable, Codable { resources = try container.decodeIfPresent(Resources.self, forKey: .resources) ?? .init() runtimeHandler = try container.decodeIfPresent(String.self, forKey: .runtimeHandler) ?? "container-runtime-linux" virtualization = try container.decodeIfPresent(Bool.self, forKey: .virtualization) ?? false + ssh = try container.decodeIfPresent(Bool.self, forKey: .ssh) ?? false } public struct DNSConfiguration: Sendable, Codable { diff --git a/Sources/ContainerClient/Flags.swift b/Sources/ContainerClient/Flags.swift index 98854954..b3b0e09b 100644 --- a/Sources/ContainerClient/Flags.swift +++ b/Sources/ContainerClient/Flags.swift @@ -155,6 +155,9 @@ public struct Flags { "Expose virtualization capabilities to the container. (Host must have nested virtualization support, and guest kernel must have virtualization capabilities enabled)" ) public var virtualization: Bool = false + + @Flag(name: .customLong("ssh"), help: "Forward SSH agent socket to container") + public var ssh = false } public struct Progress: ParsableArguments { diff --git a/Sources/ContainerClient/Utility.swift b/Sources/ContainerClient/Utility.swift index 2366466e..504c2195 100644 --- a/Sources/ContainerClient/Utility.swift +++ b/Sources/ContainerClient/Utility.swift @@ -208,6 +208,8 @@ public struct Utility { // to enable socket forwarding from container to host. config.publishedSockets = try Parser.publishSockets(management.publishSockets) + config.ssh = management.ssh + return (config, kernel) } diff --git a/Sources/Services/ContainerSandboxService/SandboxService.swift b/Sources/Services/ContainerSandboxService/SandboxService.swift index bfa2602a..25e1b2dc 100644 --- a/Sources/Services/ContainerSandboxService/SandboxService.swift +++ b/Sources/Services/ContainerSandboxService/SandboxService.swift @@ -49,6 +49,9 @@ public actor SandboxService { private var processes: [String: ProcessInfo] = [:] private var socketForwarders: [SocketForwarderResult] = [] + private static let sshAuthSocketGuestPath = "/run/host-services/ssh-auth.sock" + private static let sshAuthSocketEnvVar = "SSH_AUTH_SOCK" + /// Create an instance with a bundle that describes the container. /// /// - Parameters: @@ -718,6 +721,17 @@ public actor SandboxService { czConfig.sockets.append(socketConfig) } + if config.ssh { + if let sshSocket = Foundation.ProcessInfo.processInfo.environment[Self.sshAuthSocketEnvVar] { + let socketConfig = UnixSocketConfiguration( + source: URL(fileURLWithPath: sshSocket), + destination: URL(fileURLWithPath: Self.sshAuthSocketGuestPath), + direction: .into + ) + czConfig.sockets.append(socketConfig) + } + } + czConfig.hostname = config.id if let dns = config.dns { @@ -726,7 +740,7 @@ public actor SandboxService { searchDomains: dns.searchDomains, options: dns.options) } - Self.configureInitialProcess(czConfig: &czConfig, process: config.initProcess) + Self.configureInitialProcess(czConfig: &czConfig, config: config) } private func getDefaultNameserver(attachmentConfigurations: [AttachmentConfiguration]) async throws -> String? { @@ -744,10 +758,24 @@ public actor SandboxService { private static func configureInitialProcess( czConfig: inout LinuxContainer.Configuration, - process: ProcessConfiguration + config: ContainerConfiguration ) { + let process = config.initProcess + czConfig.process.arguments = [process.executable] + process.arguments czConfig.process.environmentVariables = process.environment + + // Add SSH_AUTH_SOCK if ssh forwarding is enabled + if config.ssh { + if czConfig.sockets.contains(where: { + $0.destination == URL(fileURLWithPath: Self.sshAuthSocketGuestPath) + }) { + if !czConfig.process.environmentVariables.contains(where: { $0.starts(with: "\(Self.sshAuthSocketEnvVar)=") }) { + czConfig.process.environmentVariables.append("\(Self.sshAuthSocketEnvVar)=\(Self.sshAuthSocketGuestPath)") + } + } + } + czConfig.process.terminal = process.terminal czConfig.process.workingDirectory = process.workingDirectory czConfig.process.rlimits = process.rlimits.map {