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
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 @@ -172,6 +173,7 @@ func (a *Common) Dependencies() []asset.Asset {
&tls.KubeletServingCABundle{},
&tls.MCSCertKey{},
&tls.IRICertKey{},
&tls.IRIRegistryCredentials{},
&tls.RootCA{},
&tls.ServiceAccountKeyPair{},
&tls.IronicTLSCert{},
Expand Down Expand Up @@ -380,10 +382,28 @@ 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.
// IRIRegistryCredentials generates credentials only when the NoRegistryClusterInstall
// feature gate is enabled and an InternalReleaseImage manifest is present.
// This ensures kubelet/CRI-O on bootstrap and cluster nodes can
Comment thread
rwsu marked this conversation as resolved.
// authenticate to the IRI registry on master nodes.
iriAuth := &tls.IRIRegistryCredentials{}
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.Fatalf("Failed to merge IRI registry credentials into pull secret: %v", err)
}
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 @@ -407,6 +427,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
26 changes: 26 additions & 0 deletions pkg/asset/manifests/operators.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ func (m *Manifests) Dependencies() []asset.Asset {
&tls.RootCA{},
&tls.MCSCertKey{},
&tls.IRICertKey{},
&tls.IRIRegistryCredentials{},
&manifests.InternalReleaseImage{},
new(rhcos.Image),

Expand All @@ -87,6 +88,7 @@ func (m *Manifests) Dependencies() []asset.Asset {
&bootkube.MachineConfigServerTLSSecret{},
&bootkube.OpenshiftConfigSecretPullSecret{},
&bootkube.InternalReleaseImageTLSSecret{},
&bootkube.InternalReleaseImageRegistryAuthSecret{},
&BMCVerifyCAConfigMap{},
}
}
Expand Down Expand Up @@ -234,6 +236,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, appendIRIRegistryCredentials(dependencies))
}
}

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

// appendIRIRegistryCredentials renders the IRI registry auth secret template with the generated credentials.
func appendIRIRegistryCredentials(dependencies asset.Parents) *asset.File {
iriAuth := &tls.IRIRegistryCredentials{}
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,
}
}

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
}
88 changes: 88 additions & 0 deletions pkg/asset/tls/iriregistryauth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
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
)

// IRIRegistryCredentials 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 IRIRegistryCredentials struct {
Username string
Password string //nolint:gosec // this is a credential holder, not a hardcoded secret
HtpasswdContent string
}

var _ asset.Asset = (*IRIRegistryCredentials)(nil)

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

// Generate generates the IRI registry authentication credentials.
func (a *IRIRegistryCredentials) 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 *IRIRegistryCredentials) Name() string {
return "IRI Registry Authentication"
}
Loading