Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add metal user and SSH Keypair during Discovery boot #219

Merged
merged 4 commits into from
Jan 21, 2025
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
2 changes: 1 addition & 1 deletion .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@ jobs:
- name: golangci-lint
uses: golangci/golangci-lint-action@v6
with:
version: v1.61
version: v1.63
6 changes: 3 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -223,10 +223,10 @@ ADDLICENSE ?= $(LOCALBIN)/addlicense-$(ADDLICENSE_VERSION)

## Tool Versions
KUSTOMIZE_VERSION ?= v5.3.0
CONTROLLER_TOOLS_VERSION ?= v0.16.3
CONTROLLER_TOOLS_VERSION ?= v0.17.1
ENVTEST_VERSION ?= latest
GOLANGCI_LINT_VERSION ?= v1.61.0
GOIMPORTS_VERSION ?= v0.25.0
GOLANGCI_LINT_VERSION ?= v1.63.0
GOIMPORTS_VERSION ?= v0.29.0
GEN_CRD_API_REFERENCE_DOCS_VERSION ?= v0.3.0
ADDLICENSE_VERSION ?= v1.1.1

Expand Down
2 changes: 1 addition & 1 deletion api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion config/crd/bases/metal.ironcore.dev_bmcs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.16.3
controller-gen.kubebuilder.io/version: v0.17.1
name: bmcs.metal.ironcore.dev
spec:
group: metal.ironcore.dev
Expand Down
2 changes: 1 addition & 1 deletion config/crd/bases/metal.ironcore.dev_bmcsecrets.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.16.3
controller-gen.kubebuilder.io/version: v0.17.1
name: bmcsecrets.metal.ironcore.dev
spec:
group: metal.ironcore.dev
Expand Down
2 changes: 1 addition & 1 deletion config/crd/bases/metal.ironcore.dev_endpoints.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.16.3
controller-gen.kubebuilder.io/version: v0.17.1
name: endpoints.metal.ironcore.dev
spec:
group: metal.ironcore.dev
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.16.3
controller-gen.kubebuilder.io/version: v0.17.1
name: serverbootconfigurations.metal.ironcore.dev
spec:
group: metal.ironcore.dev
Expand Down
2 changes: 1 addition & 1 deletion config/crd/bases/metal.ironcore.dev_serverclaims.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.16.3
controller-gen.kubebuilder.io/version: v0.17.1
name: serverclaims.metal.ironcore.dev
spec:
group: metal.ironcore.dev
Expand Down
2 changes: 1 addition & 1 deletion config/crd/bases/metal.ironcore.dev_servers.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.16.3
controller-gen.kubebuilder.io/version: v0.17.1
name: servers.metal.ironcore.dev
spec:
group: metal.ironcore.dev
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ require (
github.com/spf13/cobra v1.8.1
github.com/stmcginnis/gofish v0.20.0
golang.org/x/crypto v0.32.0
gopkg.in/yaml.v3 v3.0.1
k8s.io/api v0.31.1
k8s.io/apiextensions-apiserver v0.31.0
k8s.io/apimachinery v0.31.1
Expand Down Expand Up @@ -69,7 +70,6 @@ require (
google.golang.org/protobuf v1.36.1 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
Expand Down
21 changes: 21 additions & 0 deletions internal/controller/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,35 @@
package controller

import (
"crypto/rand"
"fmt"
"math/big"

metalv1alpha1 "github.com/ironcore-dev/metal-operator/api/v1alpha1"
"sigs.k8s.io/controller-runtime/pkg/client"
)

const (
fieldOwner = client.FieldOwner("metal.ironcore.dev/controller-manager")
)

func shouldIgnoreReconciliation(obj client.Object) bool {
val, found := obj.GetAnnotations()[metalv1alpha1.OperationAnnotation]
if !found {
return false
}
return val == metalv1alpha1.OperationAnnotationIgnore
}

func GenerateRandomPassword(length int) ([]byte, error) {
const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
result := make([]byte, length)
for i := 0; i < length; i++ {
n, err := rand.Int(rand.Reader, big.NewInt(int64(len(letters))))
if err != nil {
return nil, fmt.Errorf("failed to generate random password: %w", err)
}
result[i] = letters[n.Int64()]
}
return result, nil
}
125 changes: 101 additions & 24 deletions internal/controller/server_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,20 @@ package controller

import (
"context"
"crypto/rand"
"crypto/rsa"
"encoding/json"
"encoding/pem"
"fmt"
"io"
"net/http"
"sort"
"time"

"golang.org/x/crypto/bcrypt"

"golang.org/x/crypto/ssh"

"github.com/ironcore-dev/metal-operator/internal/bmcutils"

"github.com/go-logr/logr"
Expand All @@ -37,9 +44,13 @@ import (
)

const (
DefaultIgnitionSecretKeyName = "ignition"
DefaultIgnitionFormatKey = "format"
DefaultIgnitionFormatValue = "fcos"
DefaultIgnitionSecretKeyName = "ignition"
DefaultIgnitionFormatKey = "format"
DefaultIgnitionFormatValue = "fcos"
SSHKeyPairSecretPrivateKeyName = "pem"
SSHKeyPairSecretPublicKeyName = "pub"
SShKeyPairSecretPasswordKeyName = "password"

ServerFinalizer = "metal.ironcore.dev/server"
InternalAnnotationTypeKeyName = "metal.ironcore.dev/type"
InternalAnnotationTypeValue = "Internal"
Expand Down Expand Up @@ -406,11 +417,7 @@ func (r *ServerReconciler) ensureServerBootConfigRef(ctx context.Context, server
APIVersion: "metal.ironcore.dev/v1alpha1",
Kind: "ServerBootConfiguration",
}
if err := r.Patch(ctx, server, client.MergeFrom(serverBase)); err != nil {
return err
}

return nil
return r.Patch(ctx, server, client.MergeFrom(serverBase))
}

func (r *ServerReconciler) updateServerStatus(ctx context.Context, log logr.Logger, server *metalv1alpha1.Server) error {
Expand Down Expand Up @@ -492,34 +499,104 @@ func (r *ServerReconciler) applyBootConfigurationAndIgnitionForDiscovery(ctx con
}

func (r *ServerReconciler) applyDefaultIgnitionForServer(ctx context.Context, log logr.Logger, server *metalv1alpha1.Server, bootConfig *metalv1alpha1.ServerBootConfiguration, registryURL string) error {
sshPrivateKey, sshPublicKey, password, err := generateSSHKeyPairAndPassword()
if err != nil {
return fmt.Errorf("failed to generate SSH keypair: %w", err)
}

sshSecret := &v1.Secret{
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
Kind: "Secret",
},
ObjectMeta: metav1.ObjectMeta{
Namespace: r.ManagerNamespace,
Name: fmt.Sprintf("%s-ssh", bootConfig.Name),
},
Data: map[string][]byte{
SSHKeyPairSecretPublicKeyName: sshPublicKey,
SSHKeyPairSecretPrivateKeyName: sshPrivateKey,
SShKeyPairSecretPasswordKeyName: password,
},
}
if err := controllerutil.SetControllerReference(bootConfig, sshSecret, r.Scheme); err != nil {
return fmt.Errorf("failed to set controller reference: %w", err)
}
if err := r.Patch(ctx, sshSecret, client.Apply, fieldOwner, client.ForceOwnership); err != nil {
return fmt.Errorf("failed to apply default SSH keypair: %w", err)
}
log.V(1).Info("Applied SSH keypair secret", "SSHKeyPair", client.ObjectKeyFromObject(sshSecret))

probeFlags := fmt.Sprintf("--registry-url=%s --server-uuid=%s", registryURL, server.Spec.SystemUUID)
ignitionData, err := r.generateDefaultIgnitionDataForServer(probeFlags)
ignitionData, err := r.generateDefaultIgnitionDataForServer(probeFlags, sshPublicKey, password)
if err != nil {
return fmt.Errorf("failed to generate default ignitionSecret data: %w", err)
}

ignitionSecret := &v1.Secret{}
ignitionSecret.Name = bootConfig.Name
ignitionSecret.Namespace = r.ManagerNamespace
opResult, err := controllerutil.CreateOrPatch(ctx, r.Client, ignitionSecret, func() error {
ignitionSecret.Data = map[string][]byte{
ignitionSecret := &v1.Secret{
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
Kind: "Secret",
},
ObjectMeta: metav1.ObjectMeta{
Namespace: r.ManagerNamespace,
Name: bootConfig.Name,
},
Data: map[string][]byte{
DefaultIgnitionFormatKey: []byte(DefaultIgnitionFormatValue),
DefaultIgnitionSecretKeyName: ignitionData,
}
return controllerutil.SetControllerReference(bootConfig, ignitionSecret, r.Client.Scheme())
})
if err != nil {
return fmt.Errorf("failed to create or patch Ignition Secret: %w", err)
},
}

if err := controllerutil.SetControllerReference(bootConfig, ignitionSecret, r.Scheme); err != nil {
return fmt.Errorf("failed to set controller reference: %w", err)
}
log.V(1).Info("Created or patched Ignition Secret", "Secret", ignitionSecret.Name, "Operation", opResult)

if err := r.Patch(ctx, ignitionSecret, client.Apply, fieldOwner, client.ForceOwnership); err != nil {
return fmt.Errorf("failed to apply default ignition secret: %w", err)
}
log.V(1).Info("Applied Ignition Secret")

return nil
}

func (r *ServerReconciler) generateDefaultIgnitionDataForServer(flags string) ([]byte, error) {
ignitionData, err := ignition.GenerateDefaultIgnitionData(ignition.ContainerConfig{
Image: r.ProbeImage,
Flags: flags,
func generateSSHKeyPairAndPassword() ([]byte, []byte, []byte, error) {
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we consider ed25519 or 4096-bit keys?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can make this configurable later. RSA was easier because there helper method to validate the correctness of the key.

if err != nil {
return nil, nil, nil, fmt.Errorf("failed to generate private key: %w", err)
}

privateKeyBlock, err := ssh.MarshalPrivateKey(privateKey, "")
if err != nil {
return nil, nil, nil, err
}
privateKeyPem := pem.EncodeToMemory(privateKeyBlock)

sshPubKey, err := ssh.NewPublicKey(&privateKey.PublicKey)
if err != nil {
return nil, nil, nil, fmt.Errorf("failed to create SSH public key: %w", err)
}
publicKeyAuthorized := ssh.MarshalAuthorizedKey(sshPubKey)

password, err := GenerateRandomPassword(20)
if err != nil {
return nil, nil, nil, fmt.Errorf("failed to generate password: %w", err)
}

return privateKeyPem, publicKeyAuthorized, password, nil
}

func (r *ServerReconciler) generateDefaultIgnitionDataForServer(flags string, sshPublicKey []byte, password []byte) ([]byte, error) {
passwordHash, err := bcrypt.GenerateFromPassword(password, bcrypt.DefaultCost)
if err != nil {
return nil, fmt.Errorf("failed to generate password hash: %w", err)
}

ignitionData, err := ignition.GenerateDefaultIgnitionData(ignition.Config{
Image: r.ProbeImage,
Flags: flags,
SSHPublicKey: string(sshPublicKey),
PasswordHash: string(passwordHash),
})
if err != nil {
return nil, fmt.Errorf("failed to generate default ignition data: %w", err)
Expand Down
Loading
Loading