Skip to content

Commit 18ae968

Browse files
committed
Add Allow and Block lists for registries.
1 parent c1ee57f commit 18ae968

9 files changed

Lines changed: 471 additions & 96 deletions

File tree

Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ RUN go mod download
1515
COPY cmd/main.go cmd/main.go
1616
COPY api/ api/
1717
COPY internal/controller/ internal/controller/
18+
COPY internal/registry/ internal/registry/
1819
COPY server/ server/
1920
COPY templates/ templates/
2021

api/v1alpha1/zz_generated.deepcopy.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cmd/main.go

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import (
3434

3535
bootv1alpha1 "github.com/ironcore-dev/boot-operator/api/v1alpha1"
3636
"github.com/ironcore-dev/boot-operator/internal/controller"
37+
"github.com/ironcore-dev/boot-operator/internal/registry"
3738
bootserver "github.com/ironcore-dev/boot-operator/server"
3839
//+kubebuilder:scaffold:imports
3940
)
@@ -227,6 +228,10 @@ func main() {
227228
os.Exit(1)
228229
}
229230

231+
// Initialize registry validator for OCI image validation
232+
registryValidator := registry.NewValidator()
233+
setupLog.Info("Initialized registry validator", "allowedRegistries", os.Getenv("ALLOWED_REGISTRIES"), "blockedRegistries", os.Getenv("BLOCKED_REGISTRIES"))
234+
230235
if controllers.Enabled(ipxeBootConfigController) {
231236
if err = (&controller.IPXEBootConfigReconciler{
232237
Client: mgr.GetClient(),
@@ -239,10 +244,11 @@ func main() {
239244

240245
if controllers.Enabled(serverBootConfigControllerPxe) {
241246
if err = (&controller.ServerBootConfigurationPXEReconciler{
242-
Client: mgr.GetClient(),
243-
Scheme: mgr.GetScheme(),
244-
IPXEServiceURL: ipxeServiceURL,
245-
Architecture: architecture,
247+
Client: mgr.GetClient(),
248+
Scheme: mgr.GetScheme(),
249+
IPXEServiceURL: ipxeServiceURL,
250+
Architecture: architecture,
251+
RegistryValidator: registryValidator,
246252
}).SetupWithManager(mgr); err != nil {
247253
setupLog.Error(err, "unable to create controller", "controller", "ServerBootConfigPxe")
248254
os.Exit(1)
@@ -251,10 +257,11 @@ func main() {
251257

252258
if controllers.Enabled(serverBootConfigControllerHttp) {
253259
if err = (&controller.ServerBootConfigurationHTTPReconciler{
254-
Client: mgr.GetClient(),
255-
Scheme: mgr.GetScheme(),
256-
ImageServerURL: imageServerURL,
257-
Architecture: architecture,
260+
Client: mgr.GetClient(),
261+
Scheme: mgr.GetScheme(),
262+
ImageServerURL: imageServerURL,
263+
Architecture: architecture,
264+
RegistryValidator: registryValidator,
258265
}).SetupWithManager(mgr); err != nil {
259266
setupLog.Error(err, "unable to create controller", "controller", "ServerBootConfigHttp")
260267
os.Exit(1)

internal/controller/serverbootconfiguration_http_controller.go

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
apimeta "k8s.io/apimachinery/pkg/api/meta"
1313

1414
"github.com/containerd/containerd/remotes/docker"
15+
"github.com/ironcore-dev/boot-operator/internal/registry"
1516
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
1617

1718
corev1 "k8s.io/api/core/v1"
@@ -36,9 +37,10 @@ const (
3637

3738
type ServerBootConfigurationHTTPReconciler struct {
3839
client.Client
39-
Scheme *runtime.Scheme
40-
ImageServerURL string
41-
Architecture string
40+
Scheme *runtime.Scheme
41+
ImageServerURL string
42+
Architecture string
43+
RegistryValidator *registry.Validator
4244
}
4345

4446
//+kubebuilder:rbac:groups=metal.ironcore.dev,resources=serverbootconfigurations,verbs=get;list;watch
@@ -87,6 +89,13 @@ func (r *ServerBootConfigurationHTTPReconciler) reconcile(ctx context.Context, l
8789

8890
ukiURL, err := r.constructUKIURL(ctx, config.Spec.Image)
8991
if err != nil {
92+
log.Error(err, "Failed to construct UKI URL")
93+
if patchErr := r.patchConfigStateFromHTTPState(ctx,
94+
&bootv1alpha1.HTTPBootConfig{Status: bootv1alpha1.HTTPBootConfigStatus{
95+
State: bootv1alpha1.HTTPBootConfigStateError}},
96+
config); patchErr != nil {
97+
return ctrl.Result{}, fmt.Errorf("failed to patch state to error: %w (original error: %w)", patchErr, err)
98+
}
9099
return ctrl.Result{}, fmt.Errorf("failed to construct UKI URL: %w", err)
91100
}
92101
log.V(1).Info("Extracted UKI URL for boot")
@@ -203,8 +212,14 @@ func (r *ServerBootConfigurationHTTPReconciler) constructUKIURL(ctx context.Cont
203212
}
204213

205214
func (r *ServerBootConfigurationHTTPReconciler) getUKIDigestFromNestedManifest(ctx context.Context, imageName, imageVersion string) (string, error) {
206-
resolver := docker.NewResolver(docker.ResolverOptions{})
207215
imageRef := fmt.Sprintf("%s:%s", imageName, imageVersion)
216+
if r.RegistryValidator != nil {
217+
if err := r.RegistryValidator.ValidateImageRegistry(imageRef); err != nil {
218+
return "", fmt.Errorf("registry validation failed: %w", err)
219+
}
220+
}
221+
222+
resolver := docker.NewResolver(docker.ResolverOptions{})
208223
name, desc, err := resolver.Resolve(ctx, imageRef)
209224
if err != nil {
210225
return "", fmt.Errorf("failed to resolve image reference: %w", err)

internal/controller/serverbootconfiguration_pxe_controller.go

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
"sigs.k8s.io/controller-runtime/pkg/reconcile"
3030

3131
"github.com/ironcore-dev/boot-operator/api/v1alpha1"
32+
"github.com/ironcore-dev/boot-operator/internal/registry"
3233
apimeta "k8s.io/apimachinery/pkg/api/meta"
3334
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3435
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
@@ -56,9 +57,10 @@ const (
5657

5758
type ServerBootConfigurationPXEReconciler struct {
5859
client.Client
59-
Scheme *runtime.Scheme
60-
IPXEServiceURL string
61-
Architecture string
60+
Scheme *runtime.Scheme
61+
IPXEServiceURL string
62+
Architecture string
63+
RegistryValidator *registry.Validator
6264
}
6365

6466
//+kubebuilder:rbac:groups=metal.ironcore.dev,resources=serverbootconfigurations,verbs=get;list;watch
@@ -235,8 +237,14 @@ func (r *ServerBootConfigurationPXEReconciler) getImageDetailsFromConfig(ctx con
235237
}
236238

237239
func (r *ServerBootConfigurationPXEReconciler) getLayerDigestsFromNestedManifest(ctx context.Context, imageName, imageVersion string) (string, string, string, error) {
238-
resolver := docker.NewResolver(docker.ResolverOptions{})
239240
imageRef := fmt.Sprintf("%s:%s", imageName, imageVersion)
241+
if r.RegistryValidator != nil {
242+
if err := r.RegistryValidator.ValidateImageRegistry(imageRef); err != nil {
243+
return "", "", "", fmt.Errorf("registry validation failed: %w", err)
244+
}
245+
}
246+
247+
resolver := docker.NewResolver(docker.ResolverOptions{})
240248
name, desc, err := resolver.Resolve(ctx, imageRef)
241249
if err != nil {
242250
return "", "", "", fmt.Errorf("failed to resolve image reference: %w", err)

internal/controller/suite_test.go

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@ package controller
66
import (
77
"context"
88
"fmt"
9+
"os"
910
"path/filepath"
1011
"runtime"
1112
"testing"
1213
"time"
1314

1415
"k8s.io/utils/ptr"
1516
"sigs.k8s.io/controller-runtime/pkg/config"
17+
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
1618

1719
"github.com/ironcore-dev/controller-utils/modutils"
1820
metalv1alpha1 "github.com/ironcore-dev/metal-operator/api/v1alpha1"
@@ -33,6 +35,7 @@ import (
3335
"sigs.k8s.io/controller-runtime/pkg/log/zap"
3436

3537
bootv1alpha1 "github.com/ironcore-dev/boot-operator/api/v1alpha1"
38+
"github.com/ironcore-dev/boot-operator/internal/registry"
3639
//+kubebuilder:scaffold:imports
3740
)
3841

@@ -61,6 +64,12 @@ func TestControllers(t *testing.T) {
6164
var _ = BeforeSuite(func() {
6265
logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))
6366

67+
// Set ALLOWED_REGISTRIES for tests to use ghcr.io images
68+
Expect(os.Setenv("ALLOWED_REGISTRIES", "ghcr.io")).To(Succeed())
69+
DeferCleanup(func() {
70+
Expect(os.Unsetenv("ALLOWED_REGISTRIES")).To(Succeed())
71+
})
72+
6473
By("bootstrapping test environment")
6574
testEnv = &envtest.Environment{
6675
CRDDirectoryPaths: []string{
@@ -120,6 +129,9 @@ func SetupTest() *corev1.Namespace {
120129

121130
k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{
122131
Scheme: scheme.Scheme,
132+
Metrics: metricsserver.Options{
133+
BindAddress: "0", // Disable metrics server to avoid port 8080 conflicts with Tilt
134+
},
123135
Controller: config.Controller{
124136
// need to skip unique controller name validation
125137
// since all tests need a dedicated controller
@@ -128,17 +140,21 @@ func SetupTest() *corev1.Namespace {
128140
})
129141
Expect(err).ToNot(HaveOccurred())
130142

143+
registryValidator := registry.NewValidator()
144+
131145
Expect((&ServerBootConfigurationPXEReconciler{
132-
Client: k8sManager.GetClient(),
133-
Scheme: k8sManager.GetScheme(),
134-
IPXEServiceURL: "http://localhost:5000",
135-
Architecture: "arm64",
146+
Client: k8sManager.GetClient(),
147+
Scheme: k8sManager.GetScheme(),
148+
IPXEServiceURL: "http://localhost:5000",
149+
Architecture: runtime.GOARCH,
150+
RegistryValidator: registryValidator,
136151
}).SetupWithManager(k8sManager)).To(Succeed())
137152

138153
Expect((&ServerBootConfigurationHTTPReconciler{
139-
Client: k8sManager.GetClient(),
140-
Scheme: k8sManager.GetScheme(),
141-
ImageServerURL: "http://localhost:5000/httpboot",
154+
Client: k8sManager.GetClient(),
155+
Scheme: k8sManager.GetScheme(),
156+
ImageServerURL: "http://localhost:5000/httpboot",
157+
RegistryValidator: registryValidator,
142158
}).SetupWithManager(k8sManager)).To(Succeed())
143159

144160
go func() {

internal/registry/validation.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and IronCore contributors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package registry
5+
6+
import (
7+
"fmt"
8+
"os"
9+
"strings"
10+
)
11+
12+
// Validator provides registry validation with cached environment variables.
13+
type Validator struct {
14+
allowedRegistries string
15+
blockedRegistries string
16+
}
17+
18+
// NewValidator creates a new Validator with environment variables cached at initialization.
19+
// This should be called once at startup to avoid repeated os.Getenv calls.
20+
func NewValidator() *Validator {
21+
return &Validator{
22+
allowedRegistries: os.Getenv("ALLOWED_REGISTRIES"),
23+
blockedRegistries: os.Getenv("BLOCKED_REGISTRIES"),
24+
}
25+
}
26+
27+
// ExtractRegistryDomain extracts the registry domain from an OCI image reference.
28+
func ExtractRegistryDomain(imageRef string) string {
29+
parts := strings.SplitN(imageRef, "/", 2)
30+
if len(parts) < 2 {
31+
return "registry-1.docker.io"
32+
}
33+
34+
potentialRegistry := parts[0]
35+
36+
if strings.Contains(potentialRegistry, ".") || strings.Contains(potentialRegistry, ":") || potentialRegistry == "localhost" {
37+
return potentialRegistry
38+
}
39+
40+
return "registry-1.docker.io"
41+
}
42+
43+
// isInList checks if a value is in a comma-separated list (exact match only).
44+
func isInList(registry string, list string) bool {
45+
if list == "" {
46+
return false
47+
}
48+
49+
items := strings.Split(list, ",")
50+
for _, item := range items {
51+
if strings.TrimSpace(item) == registry {
52+
return true
53+
}
54+
}
55+
return false
56+
}
57+
58+
// IsRegistryAllowed checks if a registry is allowed based on the cached allow/block lists.
59+
func (v *Validator) IsRegistryAllowed(registry string) bool {
60+
if v.allowedRegistries != "" {
61+
return isInList(registry, v.allowedRegistries)
62+
}
63+
64+
if v.blockedRegistries != "" {
65+
return !isInList(registry, v.blockedRegistries)
66+
}
67+
68+
return false
69+
}
70+
71+
// ValidateImageRegistry validates that an image reference uses an allowed registry.
72+
func (v *Validator) ValidateImageRegistry(imageRef string) error {
73+
registry := ExtractRegistryDomain(imageRef)
74+
if !v.IsRegistryAllowed(registry) {
75+
if v.allowedRegistries != "" {
76+
return fmt.Errorf("registry not allowed: %s (allowed registries: %s)", registry, v.allowedRegistries)
77+
} else if v.blockedRegistries != "" {
78+
return fmt.Errorf("registry blocked: %s (blocked registries: %s)", registry, v.blockedRegistries)
79+
}
80+
return fmt.Errorf("registry not allowed: %s (no ALLOWED_REGISTRIES or BLOCKED_REGISTRIES configured, denying all)", registry)
81+
}
82+
return nil
83+
}
84+
85+
// Package-level functions for backward compatibility (deprecated - use Validator instead)
86+
87+
// IsRegistryAllowed checks if a registry is allowed (deprecated: use Validator.IsRegistryAllowed).
88+
func IsRegistryAllowed(registry string) bool {
89+
v := NewValidator()
90+
return v.IsRegistryAllowed(registry)
91+
}
92+
93+
// ValidateImageRegistry validates that an image reference uses an allowed registry (deprecated: use Validator.ValidateImageRegistry).
94+
func ValidateImageRegistry(imageRef string) error {
95+
v := NewValidator()
96+
return v.ValidateImageRegistry(imageRef)
97+
}

0 commit comments

Comments
 (0)