Information disclosure / Improper access control

MEDIUM
kubernetes/kubernetes
Commit: 98e798c57b0a
Affected: v1.36.0-beta.0 and earlier in the v1.36.x line
2026-06-01 22:19 UTC

Description

This commit hardens kubeadm's dry-run handling of CA certificate materials during the init phase. Key changes include: (1) creating the destination dry-run directory with restrictive permissions (0700) before copying CA artifacts, and (2) copying the actual CA certificate and key from their source paths (rather than a fixed constant path) into the dry-run location. Prior behavior could leave CA material in a dry-run directory with permissive permissions or copy from a potentially incorrect fixed path, increasing the risk of information disclosure or improper access control. The added test verifies that the CA certs/keys are indeed copied into the dry-run directory. Overall, this appears to be a genuine security hardening fix around handling of sensitive CA material in dry-run mode.

Proof of Concept

# Proof-of-concept demonstrating the information-disclosure risk that this patch mitigates # This PoC is a simulated scenario showing the difference in directory permissions and access # to CA material in a pre-fix (insecure) vs post-fix (secure) dry-run scenario. # 1) Setup a simulated CA material source SRC_DIR=$(mktemp -d) CA_BASE="root-ca" printf 'DUMMY_CERT' > "$SRC_DIR/${CA_BASE}.crt" printf 'DUMMY_KEY' > "$SRC_DIR/${CA_BASE}.key" # 2) Simulate the dry-run destination directory (pre-fix scenario would often be created with default perms, e.g., 0755) DRY_RUN_DIR_PRE=$(mktemp -d) chmod 0755 "$DRY_RUN_DIR_PRE" # insecure: world-readable # Simulate copying using the pre-fix logic cp "$SRC_DIR/${CA_BASE}.crt" "$DRY_RUN_DIR_PRE/${CA_BASE}.crt" cp "$SRC_DIR/${CA_BASE}.key" "$DRY_RUN_DIR_PRE/${CA_BASE}.key" # Demonstrate that another user could read the copied materials (pre-fix scenario) # Replace 'otheruser' with a non-privileged user available on the system. # Example: sudo -u nobody cat "$DRY_RUN_DIR_PRE/${CA_BASE}.crt" # If the environment allows switching users, this should succeed here, illustrating exposure. echo "[PRE-FIX] Directory permissions: $(stat -c '%A %a' "$DRY_RUN_DIR_PRE")" if command -v sudo >/dev/null 2>&1; then echo "Attempting read as a non-owner (pre-fix):" if sudo -u nobody bash -c 'cat "$DRY_RUN_DIR_PRE/${CA_BASE}.crt"' 2>/dev/null; then echo "Non-owner CAN read the certificate (pre-fix)." else echo "Non-owner CANNOT read the certificate (pre-fix)." fi fi # 3) Now simulate the post-fix scenario where the dry-run directory is 0700 DRY_RUN_DIR_POST=$(mktemp -d) chmod 0700 "$DRY_RUN_DIR_POST" # secure: only owner can access cp "$SRC_DIR/${CA_BASE}.crt" "$DRY_RUN_DIR_POST/${CA_BASE}.crt" cp "$SRC_DIR/${CA_BASE}.key" "$DRY_RUN_DIR_POST/${CA_BASE}.key" echo "[POST-FIX] Directory permissions: $(stat -c '%A %a' "$DRY_RUN_DIR_POST")" if command -v sudo >/dev/null 2>&1; then echo "Attempting read as a non-owner (post-fix):" if sudo -u nobody bash -c 'cat "$DRY_RUN_DIR_POST/${CA_BASE}.crt"' 2>/dev/null; then echo "Non-owner CAN read the certificate (post-fix)." else echo "Non-owner CANNOT read the certificate (post-fix)." fi fi # Cleanup (optional) rm -rf "$SRC_DIR" "$DRY_RUN_DIR_PRE" "$DRY_RUN_DIR_POST"

Commit Details

Author: bo.jiang

Date: 2026-05-28 04:43 UTC

Message:

kubeadm: fix dry-run CA copy paths in init certs Signed-off-by: bo.jiang <bo.jiang@daocloud.io>

Triage Assessment

Vulnerability Type: Information disclosure / Improper access control

Confidence: MEDIUM

Reasoning:

The change ensures secure handling of certificate and key files during kubeadm's dry-run by creating the destination directory with restrictive permissions (0700) before copying and by copying the actual source certificate/key paths instead of a possibly incorrect constant path. This reduces the risk of unauthorized access to sensitive CA material in a dry-run scenario and fixes potential path/permission issues when copying CA artifacts. The test adds verification that certs/keys are copied to the dry-run directory, reinforcing the security-related behavior.

Verification Assessment

Vulnerability Type: Information disclosure / Improper access control

Confidence: MEDIUM

Affected Versions: v1.36.0-beta.0 and earlier in the v1.36.x line

Code Diff

diff --git a/cmd/kubeadm/app/cmd/phases/init/certs.go b/cmd/kubeadm/app/cmd/phases/init/certs.go index d7b79524da54a..fc4f12873c823 100644 --- a/cmd/kubeadm/app/cmd/phases/init/certs.go +++ b/cmd/kubeadm/app/cmd/phases/init/certs.go @@ -18,6 +18,7 @@ package phases import ( "fmt" + "os" "path/filepath" "strings" @@ -216,20 +217,25 @@ func runCAPhase(ca *certsphase.KubeadmCert) func(c workflow.RunData) error { if cert, err := pkiutil.TryLoadCertFromDisk(data.CertificateDir(), ca.BaseName); err == nil { certsphase.CheckCertificatePeriodValidity(ca.BaseName, cert) + srcCertPath, srcKeyPath := pkiutil.PathsForCertAndKey(data.CertificateDir(), ca.BaseName) + dryRunCertPath, dryRunKeyPath := pkiutil.PathsForCertAndKey(data.CertificateWriteDir(), ca.BaseName) // If CA Cert existed while dryrun, copy CA Cert to dryrun dir for later use if data.DryRun() { - err := filesutil.CopyFile(filepath.Join(data.CertificateDir(), kubeadmconstants.CACertName), filepath.Join(data.CertificateWriteDir(), kubeadmconstants.CACertName)) + if err := os.MkdirAll(filepath.Dir(dryRunCertPath), os.FileMode(0700)); err != nil { + return errors.Wrapf(err, "failed to create directory %s", filepath.Dir(dryRunCertPath)) + } + err := filesutil.CopyFile(srcCertPath, dryRunCertPath) if err != nil { - return errors.Wrapf(err, "could not copy %s to dry run directory %s", kubeadmconstants.CACertName, data.CertificateWriteDir()) + return errors.Wrapf(err, "could not copy %s to dry run directory %s", fmt.Sprintf("%s.crt", ca.BaseName), data.CertificateWriteDir()) } } if _, err := pkiutil.TryLoadKeyFromDisk(data.CertificateDir(), ca.BaseName); err == nil { // If CA Key existed while dryrun, copy CA Key to dryrun dir for later use if data.DryRun() { - err := filesutil.CopyFile(filepath.Join(data.CertificateDir(), kubeadmconstants.CAKeyName), filepath.Join(data.CertificateWriteDir(), kubeadmconstants.CAKeyName)) + err := filesutil.CopyFile(srcKeyPath, dryRunKeyPath) if err != nil { - return errors.Wrapf(err, "could not copy %s to dry run directory %s", kubeadmconstants.CAKeyName, data.CertificateWriteDir()) + return errors.Wrapf(err, "could not copy %s to dry run directory %s", fmt.Sprintf("%s.key", ca.BaseName), data.CertificateWriteDir()) } } fmt.Printf("[certs] Using existing %s certificate authority\n", ca.BaseName) diff --git a/cmd/kubeadm/app/cmd/phases/init/certs_test.go b/cmd/kubeadm/app/cmd/phases/init/certs_test.go index 66628a0de8e19..96ad066153d5c 100644 --- a/cmd/kubeadm/app/cmd/phases/init/certs_test.go +++ b/cmd/kubeadm/app/cmd/phases/init/certs_test.go @@ -17,14 +17,18 @@ limitations under the License. package phases import ( + "os" + "path/filepath" "testing" "github.com/spf13/cobra" kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" "k8s.io/kubernetes/cmd/kubeadm/app/cmd/phases/workflow" + certsphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/certs" certstestutil "k8s.io/kubernetes/cmd/kubeadm/app/util/certs/testing" configutil "k8s.io/kubernetes/cmd/kubeadm/app/util/config/testing" + "k8s.io/kubernetes/cmd/kubeadm/app/util/pkiutil" pkiutiltesting "k8s.io/kubernetes/cmd/kubeadm/app/util/pkiutil/testing" ) @@ -33,10 +37,19 @@ type testCertsData struct { cfg *kubeadmapi.InitConfiguration } +type testDryRunCertsData struct { + testCertsData + certificateDir string + certificateWriteDir string +} + func (t *testCertsData) Cfg() *kubeadmapi.InitConfiguration { return t.cfg } func (t *testCertsData) ExternalCA() bool { return false } func (t *testCertsData) CertificateDir() string { return t.cfg.CertificatesDir } func (t *testCertsData) CertificateWriteDir() string { return t.cfg.CertificatesDir } +func (t *testDryRunCertsData) DryRun() bool { return true } +func (t *testDryRunCertsData) CertificateDir() string { return t.certificateDir } +func (t *testDryRunCertsData) CertificateWriteDir() string { return t.certificateWriteDir } func TestCreateSparseCerts(t *testing.T) { for _, test := range certstestutil.GetSparseCertTestCases(t) { @@ -63,3 +76,45 @@ func TestCreateSparseCerts(t *testing.T) { }) } } + +func TestRunCAPhaseCopiesExistingCAFilesToDryRunDir(t *testing.T) { + for _, ca := range []*certsphase.KubeadmCert{ + certsphase.KubeadmCertRootCA(), + certsphase.KubeadmCertFrontProxyCA(), + certsphase.KubeadmCertEtcdCA(), + } { + t.Run(ca.Name, func(t *testing.T) { + pkiutiltesting.Reset() + + sourceDir := t.TempDir() + writeDir := t.TempDir() + caCert, caKey := certstestutil.SetupCertificateAuthority(t) + certPath, _ := pkiutil.PathsForCertAndKey(sourceDir, ca.BaseName) + if err := os.MkdirAll(filepath.Dir(certPath), os.FileMode(0700)); err != nil { + t.Fatalf("failed to create source directory for %s: %v", ca.BaseName, err) + } + if err := pkiutil.WriteCertAndKey(sourceDir, ca.BaseName, caCert, caKey); err != nil { + t.Fatalf("failed to write source CA files for %s: %v", ca.BaseName, err) + } + + cfg := configutil.GetDefaultInternalConfig(t) + cfg.CertificatesDir = sourceDir + data := &testDryRunCertsData{ + testCertsData: testCertsData{cfg: cfg}, + certificateDir: sourceDir, + certificateWriteDir: writeDir, + } + + if err := runCAPhase(ca)(data); err != nil { + t.Fatalf("runCAPhase(%s) returned error: %v", ca.Name, err) + } + + if _, err := pkiutil.TryLoadCertFromDisk(writeDir, ca.BaseName); err != nil { + t.Fatalf("expected copied cert for %s in dry-run dir: %v", ca.BaseName, err) + } + if _, err := pkiutil.TryLoadKeyFromDisk(writeDir, ca.BaseName); err != nil { + t.Fatalf("expected copied key for %s in dry-run dir: %v", ca.BaseName, err) + } + }) + } +}
← Back to Alerts View on GitHub →