Skip to content

Commit 02bd878

Browse files
authored
feat(registry): Add custom ca certificate override (#402)
Closely related to apple/container#305 I would like to override the used SSL TrustRoots via standard env variables. This here would add this configuration and would give an entrypoint for an implementation of 305 to provide CLI flags or similar. This has no tests yet, as this would require setting up something like a MITM proxy when testing against a registry. As I am unfamiliar with the codebase, I would be willing to do this, but would require a first nudge on where to best implement this. To actually use this, we would need to add the allowed env variables to the `container system start` command env filter.
1 parent dcbc7bf commit 02bd878

File tree

7 files changed

+63
-12
lines changed

7 files changed

+63
-12
lines changed

Package.resolved

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ let package = Package(
4646
.package(url: "https://github.com/swift-server/async-http-client.git", from: "1.20.1"),
4747
.package(url: "https://github.com/apple/swift-system.git", from: "1.4.0"),
4848
.package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.1.0"),
49+
.package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.36.0"),
4950
],
5051
targets: [
5152
.target(
@@ -231,6 +232,8 @@ let package = Package(
231232
"ContainerizationError",
232233
.product(name: "Collections", package: "swift-collections"),
233234
.product(name: "Logging", package: "swift-log"),
235+
.product(name: "NIOSSL", package: "swift-nio-ssl"),
236+
234237
]
235238
),
236239
.testTarget(

Sources/Containerization/Image/ImageStore/ImageStore.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ extension ImageStore {
237237
) async throws -> Image {
238238

239239
let matcher = createPlatformMatcher(for: platform)
240-
let client = try RegistryClient(reference: reference, insecure: insecure, auth: auth)
240+
let client = try RegistryClient(reference: reference, insecure: insecure, auth: auth, tlsConfiguration: TLSUtils.makeEnvironmentAwareTLSConfiguration())
241241

242242
let ref = try Reference.parse(reference)
243243
let name = ref.path
@@ -289,7 +289,7 @@ extension ImageStore {
289289
guard let tag = ref.tag ?? ref.digest else {
290290
throw ContainerizationError(.invalidArgument, message: "invalid tag/digest for image reference \(reference)")
291291
}
292-
let client = try RegistryClient(reference: reference, insecure: insecure, auth: auth)
292+
let client = try RegistryClient(reference: reference, insecure: insecure, auth: auth, tlsConfiguration: TLSUtils.makeEnvironmentAwareTLSConfiguration())
293293
let operation = ExportOperation(name: name, tag: tag, contentStore: self.contentStore, client: client, progress: progress)
294294
try await operation.export(index: img.descriptor, platforms: matcher)
295295
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
//===----------------------------------------------------------------------===//
2+
// Copyright © 2025 Apple Inc. and the Containerization project authors.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// https://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
//===----------------------------------------------------------------------===//
16+
17+
import Foundation
18+
import NIO
19+
import NIOSSL
20+
21+
public enum TLSUtils {
22+
23+
public static func makeEnvironmentAwareTLSConfiguration() -> TLSConfiguration {
24+
var tlsConfig = TLSConfiguration.makeClientConfiguration()
25+
26+
// Check standard SSL environment variables in priority order
27+
let customCAPath =
28+
ProcessInfo.processInfo.environment["SSL_CERT_FILE"]
29+
?? ProcessInfo.processInfo.environment["CURL_CA_BUNDLE"]
30+
?? ProcessInfo.processInfo.environment["REQUESTS_CA_BUNDLE"]
31+
32+
if let caPath = customCAPath {
33+
tlsConfig.trustRoots = .file(caPath)
34+
}
35+
// else: use .default
36+
37+
return tlsConfig
38+
}
39+
}

Sources/ContainerizationOCI/Client/RegistryClient.swift

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import Foundation
2222
import Logging
2323
import NIO
2424
import NIOHTTP1
25+
import NIOSSL
2526

2627
#if os(macOS)
2728
import Network
@@ -66,7 +67,8 @@ public final class RegistryClient: ContentClient {
6667
reference: String,
6768
insecure: Bool = false,
6869
auth: Authentication? = nil,
69-
logger: Logger? = nil
70+
tlsConfiguration: TLSConfiguration? = nil,
71+
logger: Logger? = nil,
7072
) throws {
7173
let ref = try Reference.parse(reference)
7274
guard let domain = ref.resolvedDomain else {
@@ -86,7 +88,8 @@ public final class RegistryClient: ContentClient {
8688
scheme: scheme,
8789
port: port,
8890
authentication: auth,
89-
retryOptions: Self.defaultRetryOptions
91+
retryOptions: Self.defaultRetryOptions,
92+
tlsConfiguration: tlsConfiguration,
9093
)
9194
}
9295

@@ -98,7 +101,8 @@ public final class RegistryClient: ContentClient {
98101
clientID: String? = nil,
99102
retryOptions: RetryOptions? = nil,
100103
bufferSize: Int = Int(4.mib()),
101-
logger: Logger? = nil
104+
tlsConfiguration: TLSConfiguration? = nil,
105+
logger: Logger? = nil,
102106
) {
103107
var components = URLComponents()
104108
components.scheme = scheme
@@ -118,6 +122,9 @@ public final class RegistryClient: ContentClient {
118122
let proxyPort = proxyURL.port ?? (proxyURL.scheme == "https" ? 443 : 80)
119123
httpConfiguration.proxy = HTTPClient.Configuration.Proxy.server(host: proxyHost, port: proxyPort)
120124
}
125+
if tlsConfiguration != nil {
126+
httpConfiguration.tlsConfiguration = tlsConfiguration
127+
}
121128

122129
if let logger {
123130
self.client = HTTPClient(eventLoopGroupProvider: .singleton, configuration: httpConfiguration, backgroundActivityLogger: logger)

Sources/cctl/LoginCommand.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import ArgumentParser
1818
import Containerization
1919
import ContainerizationError
20+
import ContainerizationExtras
2021
import ContainerizationOCI
2122
import Foundation
2223

@@ -74,7 +75,8 @@ extension Application {
7475
shouldRetry: ({ response in
7576
response.status.code >= 500
7677
})
77-
)
78+
),
79+
tlsConfiguration: TLSUtils.makeEnvironmentAwareTLSConfiguration(),
7880
)
7981
try await client.ping()
8082
try keychain.save(domain: server, username: username, password: password)

vminitd/Package.resolved

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)