Skip to content

Commit 9b3e399

Browse files
authored
Only add auth to tink-worker image pulls for matching registries: (#282)
## Description This is necessary to avoid adding auth to the tink worker image pulls that are not from the registry that requires authentication. Because when auth is added to public registries, the image pull will fail. This allows a private registry to be used in conjunction with public registries that don't require authentication. ## Why is this needed Fixes: # ## How Has This Been Tested? ## How are existing users impacted? What migration steps/scripts do we need? ## Checklist: I have: - [ ] updated the documentation and/or roadmap (if required) - [ ] added unit or e2e tests - [ ] provided instructions on how to upgrade
2 parents 1029df0 + a80f721 commit 9b3e399

File tree

4 files changed

+737
-2
lines changed

4 files changed

+737
-2
lines changed

images/hook-bootkit/go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ require (
1010
github.com/go-logr/logr v1.4.3
1111
github.com/go-logr/zerologr v1.2.3
1212
github.com/rs/zerolog v1.34.0
13+
golang.org/x/text v0.27.0
1314
)
1415

1516
require (

images/hook-bootkit/main.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,8 +132,9 @@ func run(ctx context.Context, log logr.Logger) error {
132132

133133
authStr := base64.URLEncoding.EncodeToString(encodedJSON)
134134

135-
pullOpts := image.PullOptions{
136-
RegistryAuth: authStr,
135+
pullOpts := image.PullOptions{}
136+
if useAuth(imageName, cfg.registry) {
137+
pullOpts.RegistryAuth = authStr
137138
}
138139
var out io.ReadCloser
139140
imagePullOperation := func() error {

images/hook-bootkit/registry.go

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
package main
2+
3+
import (
4+
"net/url"
5+
"strconv"
6+
"strings"
7+
8+
"golang.org/x/text/unicode/norm"
9+
)
10+
11+
// useAuth determines if authentication should be used for pulling the given image.
12+
// It compares the registry hostname extracted from the image reference against the
13+
// configured registry hostname to ensure exact matching and prevent security vulnerabilities
14+
// from substring matching attacks and homograph attacks using Unicode normalization.
15+
func useAuth(imageRef, registryHost string) bool {
16+
if registryHost == "" {
17+
return false
18+
}
19+
20+
imageHost := extractRegistryHostname(imageRef)
21+
configHost := normalizeRegistryHostname(registryHost)
22+
23+
// Apply Unicode normalization to prevent homograph attacks
24+
// Use NFC (Canonical Decomposition followed by Canonical Composition)
25+
// to ensure consistent Unicode representation
26+
imageHost = norm.NFC.String(imageHost)
27+
configHost = norm.NFC.String(configHost)
28+
29+
return imageHost == configHost
30+
}
31+
32+
// extractRegistryHostname extracts the registry hostname from an image reference.
33+
// Examples:
34+
// - "registry.example.com/namespace/image:tag" -> "registry.example.com"
35+
// - "registry.example.com:5000/image" -> "registry.example.com:5000"
36+
// - "localhost:5000/image" -> "localhost:5000"
37+
// - "image" -> "docker.io" (Docker Hub default)
38+
// - "ubuntu:20.04" -> "docker.io" (Docker Hub default)
39+
func extractRegistryHostname(imageRef string) string {
40+
if imageRef == "" {
41+
return ""
42+
}
43+
44+
// Split the image reference by '/' to get the potential registry part
45+
parts := strings.Split(imageRef, "/")
46+
if len(parts) == 1 {
47+
// Single part means it's a Docker Hub image (e.g., "ubuntu", "ubuntu:20.04")
48+
return "docker.io"
49+
}
50+
51+
// The first part might be the registry hostname
52+
firstPart := parts[0]
53+
54+
// Check if the first part looks like a registry hostname
55+
// We need to distinguish between:
56+
// - Registry hostnames (registry.example.com, localhost:5000, 192.168.1.1:5000, [::1]:5000)
57+
// - Docker Hub images with tags (ubuntu:20.04, myapp:v1.2.3)
58+
// - Docker Hub usernames (username/image)
59+
if isRegistryHostname(firstPart) {
60+
return firstPart
61+
}
62+
63+
// If the first part doesn't look like a hostname, assume it's Docker Hub
64+
// Examples: "library/ubuntu", "username/image", "ubuntu:20.04"
65+
return "docker.io"
66+
}
67+
68+
// normalizeRegistryHostname normalizes a registry hostname for comparison.
69+
// It handles various formats that might be provided in configuration.
70+
// Examples:
71+
// - "https://registry.example.com" -> "registry.example.com"
72+
// - "http://localhost:5000" -> "localhost:5000"
73+
// - "registry.example.com:443" -> "registry.example.com:443"
74+
// - "registry.example.com" -> "registry.example.com"
75+
func normalizeRegistryHostname(registryHost string) string {
76+
if registryHost == "" {
77+
return ""
78+
}
79+
80+
// Handle URL schemes (https:// or http://)
81+
if strings.HasPrefix(registryHost, "https://") || strings.HasPrefix(registryHost, "http://") {
82+
parsed, err := url.Parse(registryHost)
83+
if err != nil {
84+
// If parsing fails, strip the scheme manually
85+
registryHost = strings.TrimPrefix(registryHost, "https://")
86+
registryHost = strings.TrimPrefix(registryHost, "http://")
87+
} else {
88+
registryHost = parsed.Host
89+
}
90+
}
91+
92+
// Remove any trailing path components
93+
if idx := strings.Index(registryHost, "/"); idx != -1 {
94+
registryHost = registryHost[:idx]
95+
}
96+
97+
return registryHost
98+
}
99+
100+
// isRegistryHostname determines if a string represents a registry hostname rather than
101+
// a Docker Hub image name with tag. This function handles various edge cases:
102+
// - IPv6 addresses in brackets: [::1]:5000, [2001:db8::1]:5000
103+
// - IPv4 addresses with ports: 192.168.1.1:5000
104+
// - Hostnames with ports: registry.example.com:5000, localhost:5000
105+
// - Hostnames with dots: registry.example.com, sub.domain.com
106+
// - Known registry patterns: localhost, 127.0.0.1
107+
// - Excludes Docker Hub image:tag patterns: ubuntu:20.04, myapp:v1.2.3.
108+
func isRegistryHostname(part string) bool {
109+
if part == "" {
110+
return false
111+
}
112+
113+
// Handle IPv6 addresses in brackets [::1], [2001:db8::1], [::1]:5000, or [2001:db8::1]:5000
114+
if strings.HasPrefix(part, "[") && strings.HasSuffix(part, "]") {
115+
return true
116+
}
117+
if strings.HasPrefix(part, "[") && strings.Contains(part, "]:") {
118+
return true
119+
}
120+
121+
// Check for localhost (with or without port)
122+
if part == "localhost" {
123+
return true
124+
}
125+
if strings.HasPrefix(part, "localhost:") {
126+
portStr := part[len("localhost:"):]
127+
if portStr == "" {
128+
return false
129+
}
130+
port, err := strconv.Atoi(portStr)
131+
if err != nil || port < 1 || port > 65535 {
132+
return false
133+
}
134+
return true
135+
}
136+
137+
// Check for IP addresses (IPv4) with optional port
138+
if isIPv4WithOptionalPort(part) {
139+
return true
140+
}
141+
142+
// Check if it contains a dot (indicating a domain)
143+
if strings.Contains(part, ".") {
144+
// Make sure it's not just a single dot or other invalid patterns
145+
if part == "." || part == ".." || strings.HasPrefix(part, ".") || strings.HasSuffix(part, ".") {
146+
return false
147+
}
148+
149+
// Additional check: if it contains a colon, make sure it's likely a port, not a tag
150+
if strings.Contains(part, ":") {
151+
return isHostnameWithPort(part)
152+
}
153+
154+
// Basic validation: should have at least one character before and after dot
155+
dotParts := strings.Split(part, ".")
156+
for _, dotPart := range dotParts {
157+
if len(dotPart) == 0 {
158+
return false
159+
}
160+
}
161+
162+
return true
163+
}
164+
165+
// If it contains a colon but no dot, it could be:
166+
// 1. A hostname with port (localhost:5000) - already handled above
167+
// 2. A Docker image with tag (ubuntu:20.04) - should return false
168+
// 3. An IPv4 address with port (1.2.3.4:5000) - already handled above
169+
// At this point, assume it's a Docker image with tag
170+
return false
171+
}
172+
173+
// isIPv4WithOptionalPort checks if the string is an IPv4 address with optional port.
174+
func isIPv4WithOptionalPort(part string) bool {
175+
// Split by colon to separate potential IP and port
176+
host := part
177+
if colonIndex := strings.LastIndex(part, ":"); colonIndex != -1 {
178+
host = part[:colonIndex]
179+
portStr := part[colonIndex+1:]
180+
// Validate port number (1-65535)
181+
if portStr == "" || len(portStr) > 5 {
182+
return false
183+
}
184+
port, err := strconv.Atoi(portStr)
185+
if err != nil || port < 1 || port > 65535 {
186+
return false
187+
}
188+
}
189+
190+
// Basic IPv4 validation: check for pattern like x.x.x.x
191+
parts := strings.Split(host, ".")
192+
if len(parts) != 4 {
193+
return false
194+
}
195+
196+
for _, octet := range parts {
197+
if octet == "" || len(octet) > 3 {
198+
return false
199+
}
200+
// Check if octet contains only digits
201+
for _, r := range octet {
202+
if r < '0' || r > '9' {
203+
return false
204+
}
205+
}
206+
}
207+
208+
return true
209+
}
210+
211+
// isHostnameWithPort checks if a string with both dots and colons represents
212+
// a hostname with port rather than a Docker image with tag.
213+
func isHostnameWithPort(part string) bool {
214+
// Find the last colon (potential port separator)
215+
colonIndex := strings.LastIndex(part, ":")
216+
if colonIndex == -1 {
217+
return true // No colon, just a hostname with dots
218+
}
219+
220+
portStr := part[colonIndex+1:]
221+
hostname := part[:colonIndex]
222+
223+
// Port should be numeric and within the valid range (1-65535)
224+
if len(portStr) == 0 || len(portStr) > 5 {
225+
return false
226+
}
227+
228+
port, err := strconv.Atoi(portStr)
229+
if err != nil || port < 1 || port > 65535 {
230+
return false
231+
}
232+
233+
// Hostname part should still contain dots for this to be a registry hostname
234+
return strings.Contains(hostname, ".")
235+
}

0 commit comments

Comments
 (0)