Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ RUN go mod download
COPY cmd/main.go cmd/main.go
COPY api/ api/
COPY internal/controller/ internal/controller/
COPY internal/registry/ internal/registry/
COPY server/ server/
COPY templates/ templates/

Expand Down
31 changes: 22 additions & 9 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import (

bootv1alpha1 "github.com/ironcore-dev/boot-operator/api/v1alpha1"
"github.com/ironcore-dev/boot-operator/internal/controller"
"github.com/ironcore-dev/boot-operator/internal/registry"
bootserver "github.com/ironcore-dev/boot-operator/server"
//+kubebuilder:scaffold:imports
)
Expand Down Expand Up @@ -79,6 +80,7 @@ func main() {
var ipxeServicePort int
var imageServerURL string
var architecture string
var allowedRegistries string

flag.StringVar(&architecture, "architecture", "amd64", "Target system architecture (e.g., amd64, arm64)")
flag.IntVar(&ipxeServicePort, "ipxe-service-port", 5000, "IPXE Service port to listen on.")
Expand All @@ -98,6 +100,7 @@ func main() {
flag.BoolVar(&secureMetrics, "metrics-secure", true, "If set the metrics endpoint is served securely")
flag.BoolVar(&enableHTTP2, "enable-http2", false,
"If set, HTTP/2 will be enabled for the metrics and webhook servers")
flag.StringVar(&allowedRegistries, "allowed-registries", "", "Comma-separated list of allowed OCI registries. Defaults to ghcr.io if not set.")

controllers := switches.New(
// core controllers
Expand Down Expand Up @@ -227,6 +230,14 @@ func main() {
os.Exit(1)
}

// Initialize registry validator for OCI image validation
registryValidator := registry.NewValidator(allowedRegistries)
if allowedRegistries == "" {
setupLog.Info("Initialized registry validator", "allowedRegistries", "ghcr.io (default)")
} else {
setupLog.Info("Initialized registry validator", "allowedRegistries", allowedRegistries)
}

if controllers.Enabled(ipxeBootConfigController) {
if err = (&controller.IPXEBootConfigReconciler{
Client: mgr.GetClient(),
Expand All @@ -239,10 +250,11 @@ func main() {

if controllers.Enabled(serverBootConfigControllerPxe) {
if err = (&controller.ServerBootConfigurationPXEReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
IPXEServiceURL: ipxeServiceURL,
Architecture: architecture,
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
IPXEServiceURL: ipxeServiceURL,
Architecture: architecture,
RegistryValidator: registryValidator,
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "ServerBootConfigPxe")
os.Exit(1)
Expand All @@ -251,10 +263,11 @@ func main() {

if controllers.Enabled(serverBootConfigControllerHttp) {
if err = (&controller.ServerBootConfigurationHTTPReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
ImageServerURL: imageServerURL,
Architecture: architecture,
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
ImageServerURL: imageServerURL,
Architecture: architecture,
RegistryValidator: registryValidator,
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "ServerBootConfigHttp")
os.Exit(1)
Expand Down Expand Up @@ -311,7 +324,7 @@ func main() {
}()

setupLog.Info("starting image-proxy-server")
go bootserver.RunImageProxyServer(imageProxyServerAddr, mgr.GetClient(), serverLog.WithName("imageproxyserver"))
go bootserver.RunImageProxyServer(imageProxyServerAddr, mgr.GetClient(), registryValidator, serverLog.WithName("imageproxyserver"))

setupLog.Info("starting manager")
if err := mgr.Start(ctx); err != nil {
Expand Down
4 changes: 2 additions & 2 deletions cmdutils/suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ var _ = BeforeSuite(func() {
// Note that you must have the required binaries setup under the bin directory to perform
// the tests directly. When we run make test it will be setup and used automatically.
BinaryAssetsDirectory: filepath.Join("..", "bin", "k8s",
fmt.Sprintf("1.34.0-%s-%s", runtime.GOOS, runtime.GOARCH)),
fmt.Sprintf("1.35.0-%s-%s", runtime.GOOS, runtime.GOARCH)),
}

sourceCfg, err := sourceEnv.Start()
Expand Down Expand Up @@ -89,7 +89,7 @@ var _ = BeforeSuite(func() {
// Note that you must have the required binaries setup under the bin directory to perform
// the tests directly. When we run make test it will be setup and used automatically.
BinaryAssetsDirectory: filepath.Join("..", "bin", "k8s",
fmt.Sprintf("1.34.0-%s-%s", runtime.GOOS, runtime.GOARCH)),
fmt.Sprintf("1.35.0-%s-%s", runtime.GOOS, runtime.GOARCH)),
}

// cfg is defined in this file globally.
Expand Down
40 changes: 37 additions & 3 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,55 @@ Boot Operator includes the following key components:
- Responds with an iPXE script, which the bare metal server uses to download the necessary OS components
- This endpoint is typically called directly by the server during boot and is commonly used in PXE boot scenarios


- **HTTP Boot Server**
- Handles `/httpboot` requests
- Returns a JSON response containing the location of the UKI (Unified Kernel Image) that the server should download
- The DHCP server extension typically handles the response and sends the UKI image location to the server
- Common in modern cloud-native bare metal setups, especially for containers and minimal OS images


- **Image Proxy Server**
- Handles `/image` requests
- Extracts layers from public OCI (Open Container Initiative) images, with current support for GHCR (GitHub Container Registry) only
- Downloads specific layers based on the requested URI and image specifications
- Extracts layers from OCI (Open Container Initiative) images, with support for multiple registries (e.g., GHCR, Docker Hub, and any OCI-compliant registry)
- Downloads specific layers based on the requested URI and image specifications
- Registry access is controlled via the `--allowed-registries` CLI flag (comma-separated list)
- By default (when not specified), only **ghcr.io** is allowed
- Example:
- `wget http://SERVER_ADDRESS:30007/image?imageName=ghcr.io/ironcore-dev/os-images/gardenlinux&version=1443.10&layerName=application/vnd.ironcore.image.squashfs.v1alpha1.squashfs`

- **Ignition Server**
- Handles `/ignition` requests
- Responds with Ignition configuration content tailored to the client machine, identified by its UUID in the request URL.

These servers leverage Kubernetes controllers and API objects to manage the boot process and serve requests from bare metal machines. The architecture and specifics of the controllers and API objects are described in the architecture section of the documentation.
These servers leverage Kubernetes controllers and API objects to manage the boot process and serve requests from bare metal machines. The architecture and specifics of the controllers and API objects are described in the architecture section of the documentation.

## Registry Validation

Boot Operator enforces OCI registry restrictions at two levels:

1. **Controller level (early validation):** The PXE and HTTP boot controllers validate image references against the registry allow list during reconciliation. This means misconfigured or disallowed registries are rejected immediately when a `ServerBootConfiguration` is created, providing fast feedback before any machine attempts to boot.

2. **Image Proxy Server level (runtime enforcement):** The image proxy server also validates registry domains before proxying layer downloads, acting as a second line of defense.

Registry restrictions are configured via the `--allowed-registries` CLI flag on the manager binary.

### Default Behavior

By default (when `--allowed-registries` is not set), Boot Operator allows only **ghcr.io** registry. This provides a secure-by-default configuration with zero configuration needed for the common case.

### Custom Configuration

To allow additional registries or replace the default, use the `--allowed-registries` flag with a comma-separated list:

```bash
--allowed-registries=ghcr.io,registry.example.com,quay.io
```

**Important:** When you set `--allowed-registries`, it completely replaces the default. If you want to use ghcr.io along with other registries, you must explicitly include `ghcr.io` in your list.

### Registry Matching

- Docker Hub variants (`docker.io`, `index.docker.io`, `registry-1.docker.io`) are normalized to `docker.io` for consistent matching.
- All registry domain matching is case-insensitive.
- Registries not in the allow list are denied.
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ go 1.25.6
require (
github.com/containerd/containerd v1.7.30
github.com/coreos/butane v0.27.0
github.com/distribution/reference v0.6.0
github.com/go-logr/logr v1.4.3
github.com/ironcore-dev/controller-utils v0.11.0
github.com/ironcore-dev/metal v0.0.0-20240624131301-18385f342755
github.com/ironcore-dev/metal-operator v0.3.0
github.com/onsi/ginkgo/v2 v2.28.1
github.com/onsi/gomega v1.39.1
github.com/opencontainers/go-digest v1.0.0
github.com/opencontainers/image-spec v1.1.1
github.com/spf13/cobra v1.10.2
k8s.io/api v0.35.0
Expand Down Expand Up @@ -76,7 +78,6 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_golang v1.23.2 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes=
github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k=
Expand Down
Loading
Loading