Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
apiVersion: v1
kind: Secret
metadata:
name: internal-release-image-registry-auth
namespace: openshift-machine-config-operator
annotations:
openshift.io/description: Secret containing the InternalReleaseImage registry authentication credentials
openshift.io/owning-component: Machine Config Operator
type: Opaque
data:
htpasswd: {{.IriRegistryHtpasswd}}
password: {{.IriRegistryPassword}}
47 changes: 46 additions & 1 deletion pkg/asset/ignition/bootstrap/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package bootstrap
import (
"bytes"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"fmt"
Expand Down Expand Up @@ -169,6 +170,7 @@ func (a *Common) Dependencies() []asset.Asset {
&tls.KubeletServingCABundle{},
&tls.MCSCertKey{},
&tls.IRICertKey{},
&tls.IRIRegistryAuth{},
&tls.RootCA{},
&tls.ServiceAccountKeyPair{},
&tls.IronicTLSCert{},
Expand Down Expand Up @@ -378,10 +380,27 @@ func (a *Common) getTemplateData(dependencies asset.Parents, bootstrapInPlace bo

openshiftInstallInvoker := os.Getenv("OPENSHIFT_INSTALL_INVOKER")

pullSecret := installConfig.Config.PullSecret

// Merge IRI registry credentials into pull secret if available.
// This ensures kubelet/CRI-O on bootstrap and cluster nodes can
// authenticate to the IRI registry on master nodes.
iriAuth := &tls.IRIRegistryAuth{}
dependencies.Get(iriAuth)
if iriAuth.Password != "" {
iriRegistryHost := fmt.Sprintf("api-int.%s:22625", installConfig.Config.ClusterDomain())
merged, err := mergeIRIAuthIntoPullSecret(pullSecret, iriAuth.Username, iriAuth.Password, iriRegistryHost)
if err != nil {
logrus.Warnf("Failed to merge IRI registry credentials into pull secret: %v", err)
} else {
pullSecret = merged
}
}

return &bootstrapTemplateData{
AdditionalTrustBundle: installConfig.Config.AdditionalTrustBundle,
FIPS: installConfig.Config.FIPS,
PullSecret: installConfig.Config.PullSecret,
PullSecret: pullSecret,
SSHKey: installConfig.Config.SSHKey,
ReleaseImage: releaseImage.PullSpec,
EtcdCluster: strings.Join(etcdEndpoints, ","),
Expand All @@ -404,6 +423,31 @@ func (a *Common) getTemplateData(dependencies asset.Parents, bootstrapInPlace bo
}
}

// mergeIRIAuthIntoPullSecret merges IRI registry authentication credentials
// into the pull secret so that kubelet/CRI-O can authenticate to the IRI registry.
func mergeIRIAuthIntoPullSecret(pullSecret, username, password, registryHost string) (string, error) {
var pullSecretMap map[string]interface{}
if err := json.Unmarshal([]byte(pullSecret), &pullSecretMap); err != nil {
return "", fmt.Errorf("failed to parse pull secret: %w", err)
}

auths, ok := pullSecretMap["auths"].(map[string]interface{})
if !ok {
return "", fmt.Errorf("pull secret missing 'auths' field")
}

authValue := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", username, password)))
auths[registryHost] = map[string]interface{}{
"auth": authValue,
}

mergedBytes, err := json.Marshal(pullSecretMap)
if err != nil {
return "", fmt.Errorf("failed to marshal merged pull secret: %w", err)
}
return string(mergedBytes), nil
}

// AddStorageFiles adds files to a Ignition config.
// Parameters:
// config - the ignition config to be modified
Expand Down Expand Up @@ -672,6 +716,7 @@ func (a *Common) addParentFiles(dependencies asset.Parents) {
&tls.KubeletServingCABundle{},
&tls.MCSCertKey{},
&tls.IRICertKey{},
&tls.IRIRegistryAuth{},
&tls.ServiceAccountKeyPair{},
&tls.JournalCertKey{},
&tls.IronicTLSCert{},
Expand Down
26 changes: 26 additions & 0 deletions pkg/asset/manifests/operators.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ func (m *Manifests) Dependencies() []asset.Asset {
&tls.RootCA{},
&tls.MCSCertKey{},
&tls.IRICertKey{},
&tls.IRIRegistryAuth{},
&manifests.InternalReleaseImage{},
new(rhcos.Image),

Expand All @@ -89,6 +90,7 @@ func (m *Manifests) Dependencies() []asset.Asset {
&bootkube.MachineConfigServerTLSSecret{},
&bootkube.OpenshiftConfigSecretPullSecret{},
&bootkube.InternalReleaseImageTLSSecret{},
&bootkube.InternalReleaseImageRegistryAuthSecret{},
&BMCVerifyCAConfigMap{},
}
}
Expand Down Expand Up @@ -236,6 +238,7 @@ func (m *Manifests) generateBootKubeManifests(dependencies asset.Parents) []*ass
// Skip if InternalReleaseImage manifest wasn't found.
if len(iri.FileList) > 0 {
files = append(files, appendIRIcerts(dependencies))
files = append(files, appendIRIRegistryAuth(dependencies))
}
}

Expand Down Expand Up @@ -264,6 +267,29 @@ func appendIRIcerts(dependencies asset.Parents) *asset.File {
}
}

// appendIRIRegistryAuth renders the IRI registry auth secret template with the generated credentials.
func appendIRIRegistryAuth(dependencies asset.Parents) *asset.File {
iriAuth := &tls.IRIRegistryAuth{}
iriAuthSecret := &bootkube.InternalReleaseImageRegistryAuthSecret{}
dependencies.Get(iriAuth, iriAuthSecret)

f := iriAuthSecret.Files()[0]

templateData := struct {
IriRegistryHtpasswd string
IriRegistryPassword string
}{
IriRegistryHtpasswd: base64.StdEncoding.EncodeToString([]byte(iriAuth.HtpasswdContent)),
IriRegistryPassword: base64.StdEncoding.EncodeToString([]byte(iriAuth.Password)),
}
fileData := applyTemplateData(f.Data, templateData)

return &asset.File{
Filename: path.Join(manifestDir, strings.TrimSuffix(filepath.Base(f.Filename), ".template")),
Data: fileData,
}
}
Comment on lines +270 to +291
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add nil/empty check before accessing Files()[0].

If InternalReleaseImageRegistryAuthSecret.Generate() returns early (e.g., feature gate disabled or IRI manifest missing), Files() will return an empty slice. Accessing Files()[0] would cause a panic. Although the caller checks len(iri.FileList) > 0, this function should defensively validate its own input.

🛡️ Proposed fix to add defensive check
 func appendIRIRegistryAuth(dependencies asset.Parents) *asset.File {
 	iriAuth := &tls.IRIRegistryAuth{}
 	iriAuthSecret := &bootkube.InternalReleaseImageRegistryAuthSecret{}
 	dependencies.Get(iriAuth, iriAuthSecret)
 
+	files := iriAuthSecret.Files()
+	if len(files) == 0 {
+		return nil
+	}
+	f := files[0]
-	f := iriAuthSecret.Files()[0]
 
 	templateData := struct {

Then update the caller to handle nil:

 		if len(iri.FileList) > 0 {
 			files = append(files, appendIRIcerts(dependencies))
-			files = append(files, appendIRIRegistryAuth(dependencies))
+			if authFile := appendIRIRegistryAuth(dependencies); authFile != nil {
+				files = append(files, authFile)
+			}
 		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// appendIRIRegistryAuth renders the IRI registry auth secret template with the generated credentials.
func appendIRIRegistryAuth(dependencies asset.Parents) *asset.File {
iriAuth := &tls.IRIRegistryAuth{}
iriAuthSecret := &bootkube.InternalReleaseImageRegistryAuthSecret{}
dependencies.Get(iriAuth, iriAuthSecret)
f := iriAuthSecret.Files()[0]
templateData := struct {
IriRegistryHtpasswd string
IriRegistryPassword string
}{
IriRegistryHtpasswd: base64.StdEncoding.EncodeToString([]byte(iriAuth.HtpasswdContent)),
IriRegistryPassword: base64.StdEncoding.EncodeToString([]byte(iriAuth.Password)),
}
fileData := applyTemplateData(f.Data, templateData)
return &asset.File{
Filename: path.Join(manifestDir, strings.TrimSuffix(filepath.Base(f.Filename), ".template")),
Data: fileData,
}
}
// appendIRIRegistryAuth renders the IRI registry auth secret template with the generated credentials.
func appendIRIRegistryAuth(dependencies asset.Parents) *asset.File {
iriAuth := &tls.IRIRegistryAuth{}
iriAuthSecret := &bootkube.InternalReleaseImageRegistryAuthSecret{}
dependencies.Get(iriAuth, iriAuthSecret)
files := iriAuthSecret.Files()
if len(files) == 0 {
return nil
}
f := files[0]
templateData := struct {
IriRegistryHtpasswd string
IriRegistryPassword string
}{
IriRegistryHtpasswd: base64.StdEncoding.EncodeToString([]byte(iriAuth.HtpasswdContent)),
IriRegistryPassword: base64.StdEncoding.EncodeToString([]byte(iriAuth.Password)),
}
fileData := applyTemplateData(f.Data, templateData)
return &asset.File{
Filename: path.Join(manifestDir, strings.TrimSuffix(filepath.Base(f.Filename), ".template")),
Data: fileData,
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/asset/manifests/operators.go` around lines 270 - 291, The function
appendIRIRegistryAuth dereferences iriAuthSecret.Files()[0] which can panic if
InternalReleaseImageRegistryAuthSecret.Files() returns an empty slice; add a
defensive check in appendIRIRegistryAuth to return nil immediately when
iriAuthSecret is nil or len(iriAuthSecret.Files()) == 0 before using Files()[0],
and ensure the caller that consumes appendIRIRegistryAuth handles a nil
*asset.File return (so callers of appendIRIRegistryAuth can skip adding when
nil).


func applyTemplateData(data []byte, templateData interface{}) []byte {
template := template.Must(template.New("template").Funcs(customTmplFuncs).Parse(string(data)))
buf := &bytes.Buffer{}
Expand Down
19 changes: 10 additions & 9 deletions pkg/asset/store/assetcreate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,15 +116,16 @@ func TestCreatedAssetsAreNotDirty(t *testing.T) {
}

emptyAssets := map[string]bool{
"Arbiter Ignition Config": true, // no files for non arbiter cluster
"Arbiter Machines": true, // no files for the 'none' platform
"Master Machines": true, // no files for the 'none' platform
"Worker Machines": true, // no files for the 'none' platform
"Cluster API Manifests": true, // no files for the 'none' platform and ClusterAPIInstall feature gate not set
"Cluster API Machine Manifests": true, // no files for the 'none' platform and ClusterAPIInstall feature gate not set
"Metadata": true, // read-only
"Kubeadmin Password": true, // read-only
"InternalReleaseImageTLSSecret": true, // no files when NoRegistryClusterInstall feature gate is not set
"Arbiter Ignition Config": true, // no files for non arbiter cluster
"Arbiter Machines": true, // no files for the 'none' platform
"Master Machines": true, // no files for the 'none' platform
"Worker Machines": true, // no files for the 'none' platform
"Cluster API Manifests": true, // no files for the 'none' platform and ClusterAPIInstall feature gate not set
"Cluster API Machine Manifests": true, // no files for the 'none' platform and ClusterAPIInstall feature gate not set
"Metadata": true, // read-only
"Kubeadmin Password": true, // read-only
"InternalReleaseImageTLSSecret": true, // no files when NoRegistryClusterInstall feature gate is not set
"InternalReleaseImageRegistryAuthSecret": true, // no files when NoRegistryClusterInstall feature gate is not set
}
for _, a := range tc.targets {
name := a.Name()
Expand Down
1 change: 1 addition & 0 deletions pkg/asset/targets/targets.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ var (
&openshift.RoleCloudCredsSecretReader{},
&openshift.AzureCloudProviderSecret{},
&bootkube.InternalReleaseImageTLSSecret{},
&bootkube.InternalReleaseImageRegistryAuthSecret{},
}

// IgnitionConfigs are the ignition-configs targeted assets.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package bootkube

import (
"context"
"os"
"path/filepath"

"github.com/openshift/api/features"
"github.com/openshift/installer/pkg/asset"
"github.com/openshift/installer/pkg/asset/installconfig"
"github.com/openshift/installer/pkg/asset/templates/content"
"github.com/openshift/installer/pkg/asset/templates/content/manifests"
)

const (
internalReleaseImageRegistryAuthSecretFileName = "internal-release-image-registry-auth-secret.yaml.template"
)

var _ asset.WritableAsset = (*InternalReleaseImageRegistryAuthSecret)(nil)

// InternalReleaseImageRegistryAuthSecret is the constant to represent contents of internal-release-image-registry-auth-secret.yaml.template file.
type InternalReleaseImageRegistryAuthSecret struct {
FileList []*asset.File
}

// Dependencies returns all of the dependencies directly needed by the asset.
func (t *InternalReleaseImageRegistryAuthSecret) Dependencies() []asset.Asset {
return []asset.Asset{
&installconfig.InstallConfig{},
&manifests.InternalReleaseImage{},
}
}

// Name returns the human-friendly name of the asset.
func (t *InternalReleaseImageRegistryAuthSecret) Name() string {
return "InternalReleaseImageRegistryAuthSecret"
}

// Generate generates the actual files by this asset.
func (t *InternalReleaseImageRegistryAuthSecret) Generate(_ context.Context, dependencies asset.Parents) error {
installConfig := &installconfig.InstallConfig{}
iri := &manifests.InternalReleaseImage{}

dependencies.Get(installConfig, iri)

if !installConfig.Config.EnabledFeatureGates().Enabled(features.FeatureGateNoRegistryClusterInstall) {
return nil
}

// Skip if InternalReleaseImage manifest wasn't found.
if len(iri.FileList) == 0 {
return nil
}

fileName := internalReleaseImageRegistryAuthSecretFileName
data, err := content.GetBootkubeTemplate(fileName)
if err != nil {
return err
}
t.FileList = []*asset.File{
{
Filename: filepath.Join(content.TemplateDir, fileName),
Data: data,
},
}
return nil
}

// Files returns the files generated by the asset.
func (t *InternalReleaseImageRegistryAuthSecret) Files() []*asset.File {
return t.FileList
}

// Load returns the asset from disk.
func (t *InternalReleaseImageRegistryAuthSecret) Load(f asset.FileFetcher) (bool, error) {
file, err := f.FetchByName(filepath.Join(content.TemplateDir, internalReleaseImageRegistryAuthSecretFileName))
if err != nil {
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
t.FileList = []*asset.File{file}
return true, nil
}
98 changes: 98 additions & 0 deletions pkg/asset/tls/iriregistryauth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package tls //nolint:revive // pre-existing package name

import (
"context"
"crypto/rand"
"encoding/base64"
"fmt"

"golang.org/x/crypto/bcrypt"

features "github.com/openshift/api/features"
"github.com/openshift/installer/pkg/asset"
"github.com/openshift/installer/pkg/asset/installconfig"
"github.com/openshift/installer/pkg/asset/templates/content/manifests"
)

const (
// IRIRegistryUsername is the fixed username for IRI registry authentication.
IRIRegistryUsername = "openshift"
// PasswordBytes is the number of random bytes to generate for the password (256-bit entropy).
PasswordBytes = 32
)

// IRIRegistryAuth is the asset for the IRI registry authentication credentials.
// This is an in-memory-only asset: credentials are consumed by other assets
// (operators.go, bootstrap/common.go) but not written to disk.
//
// This must NOT write files to the auth/ directory. In agent-based installs,
// assisted-service moves kubeadmin-password and kubeconfig out of auth/ and
// then calls os.Remove("auth") to delete the directory. That call fails if
// any extra files remain, which would break the deployment. See:
// https://github.com/openshift/assisted-service/blob/89897ade7135/internal/ignition/installmanifests.go#L356
type IRIRegistryAuth struct {
Username string
Password string //nolint:gosec // this is a credential holder, not a hardcoded secret
HtpasswdContent string
}

var _ asset.WritableAsset = (*IRIRegistryAuth)(nil)

// Dependencies returns the dependencies for generating IRI registry auth.
func (a *IRIRegistryAuth) Dependencies() []asset.Asset {
return []asset.Asset{
&installconfig.InstallConfig{},
&manifests.InternalReleaseImage{},
}
}

// Generate generates the IRI registry authentication credentials.
func (a *IRIRegistryAuth) Generate(ctx context.Context, dependencies asset.Parents) error {
installConfig := &installconfig.InstallConfig{}
iri := &manifests.InternalReleaseImage{}
dependencies.Get(installConfig, iri)

// Only generate if NoRegistryClusterInstall feature is enabled
if !installConfig.Config.EnabledFeatureGates().Enabled(features.FeatureGateNoRegistryClusterInstall) {
return nil
}

// Skip if InternalReleaseImage manifest wasn't found
if len(iri.FileList) == 0 {
return nil
}

// Generate random password (32 bytes = 256-bit entropy)
passwordBytes := make([]byte, PasswordBytes)
if _, err := rand.Read(passwordBytes); err != nil {
return fmt.Errorf("failed to generate random password: %w", err)
}
a.Password = base64.StdEncoding.EncodeToString(passwordBytes)
a.Username = IRIRegistryUsername

// Create bcrypt hash
hash, err := bcrypt.GenerateFromPassword([]byte(a.Password), bcrypt.DefaultCost)
if err != nil {
return fmt.Errorf("failed to hash password: %w", err)
}

// Create htpasswd format: username:bcrypt-hash
a.HtpasswdContent = fmt.Sprintf("%s:%s\n", a.Username, string(hash))

return nil
}

// Name returns the human-friendly name of the asset.
func (a *IRIRegistryAuth) Name() string {
return "IRI Registry Authentication"
}

// Files returns an empty list as this is an in-memory-only asset.
func (a *IRIRegistryAuth) Files() []*asset.File {
return []*asset.File{}
}

// Load returns false as this asset is not persisted to disk.
func (a *IRIRegistryAuth) Load(f asset.FileFetcher) (bool, error) {
return false, nil
}
Loading