Skip to content

Commit 134538d

Browse files
kvapsclaude
andauthored
feat(commands): add rotate-ca command for CA rotation (#101)
Add new rotate-ca command that: - Discovers all cluster nodes via Kubernetes API - Verifies connectivity to all nodes via Talos API - Rotates Talos and/or Kubernetes CAs - Updates talosconfig, secrets.yaml, and kubeconfig - Automatically updates encrypted versions of config files Also refactor encryption logic into encryption_helpers.go: - SaveTalosconfigWithEncryption() - SaveSecretsBundleWithEncryption() - UpdateKubeconfigEncryption() Signed-off-by: Andrei Kvapil <kvapss@gmail.com> Co-authored-by: Claude <noreply@anthropic.com>
1 parent 2eba4fa commit 134538d

5 files changed

Lines changed: 473 additions & 25 deletions

File tree

pkg/commands/encryption_helpers.go

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
// Copyright Cozystack Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package commands
16+
17+
import (
18+
"fmt"
19+
"os"
20+
"path/filepath"
21+
"strings"
22+
23+
"github.com/cozystack/talm/pkg/age"
24+
clientconfig "github.com/siderolabs/talos/pkg/machinery/client/config"
25+
"github.com/siderolabs/talos/pkg/machinery/config/generate/secrets"
26+
"gopkg.in/yaml.v3"
27+
)
28+
29+
// SaveTalosconfigWithEncryption saves talosconfig and updates talosconfig.encrypted if it exists.
30+
func SaveTalosconfigWithEncryption(config *clientconfig.Config, talosconfigPath string) error {
31+
// Save the talosconfig
32+
if err := config.Save(talosconfigPath); err != nil {
33+
return fmt.Errorf("failed to save talosconfig: %w", err)
34+
}
35+
36+
// Update talosconfig.encrypted if it exists
37+
encryptedPath := filepath.Join(Config.RootDir, "talosconfig.encrypted")
38+
if fileExists(encryptedPath) {
39+
fmt.Fprintf(os.Stderr, "Updating talosconfig.encrypted...\n")
40+
if err := age.EncryptYAMLFile(Config.RootDir, "talosconfig", "talosconfig.encrypted"); err != nil {
41+
return fmt.Errorf("failed to encrypt talosconfig: %w", err)
42+
}
43+
}
44+
45+
return nil
46+
}
47+
48+
// UpdateKubeconfigEncryption updates kubeconfig.encrypted if it exists.
49+
// kubeconfigPath should be an absolute path to the kubeconfig file.
50+
// Returns nil if encrypted file doesn't exist or key is missing.
51+
func UpdateKubeconfigEncryption(kubeconfigPath string) error {
52+
// Get relative path from project root
53+
rootAbs, err := filepath.Abs(Config.RootDir)
54+
if err != nil {
55+
return nil // Skip encryption if we can't get absolute path
56+
}
57+
58+
relKubeconfigPath, err := filepath.Rel(rootAbs, kubeconfigPath)
59+
if err != nil || strings.HasPrefix(relKubeconfigPath, "..") {
60+
return nil // Skip encryption if path is outside project root
61+
}
62+
63+
encryptedKubeconfigPath := relKubeconfigPath + ".encrypted"
64+
encryptedKubeconfigFile := filepath.Join(Config.RootDir, encryptedKubeconfigPath)
65+
keyFile := filepath.Join(Config.RootDir, "talm.key")
66+
67+
if !fileExists(encryptedKubeconfigFile) || !fileExists(keyFile) {
68+
return nil // Skip if encrypted file or key doesn't exist
69+
}
70+
71+
fmt.Fprintf(os.Stderr, "Updating %s...\n", encryptedKubeconfigPath)
72+
if err := age.EncryptYAMLFile(Config.RootDir, relKubeconfigPath, encryptedKubeconfigPath); err != nil {
73+
return fmt.Errorf("failed to encrypt kubeconfig: %w", err)
74+
}
75+
76+
return nil
77+
}
78+
79+
// UpdateTalosconfigEncryption updates talosconfig.encrypted if it exists.
80+
// Returns nil if encrypted file doesn't exist or key is missing.
81+
func UpdateTalosconfigEncryption() error {
82+
encryptedPath := filepath.Join(Config.RootDir, "talosconfig.encrypted")
83+
keyFile := filepath.Join(Config.RootDir, "talm.key")
84+
85+
if !fileExists(encryptedPath) || !fileExists(keyFile) {
86+
return nil // Skip if encrypted file or key doesn't exist
87+
}
88+
89+
fmt.Fprintf(os.Stderr, "Updating talosconfig.encrypted...\n")
90+
if err := age.EncryptYAMLFile(Config.RootDir, "talosconfig", "talosconfig.encrypted"); err != nil {
91+
return fmt.Errorf("failed to encrypt talosconfig: %w", err)
92+
}
93+
94+
return nil
95+
}
96+
97+
// SaveSecretsBundleWithEncryption saves secrets.yaml and updates secrets.encrypted.yaml if it exists.
98+
func SaveSecretsBundleWithEncryption(bundle *secrets.Bundle) error {
99+
secretsPath := ResolveSecretsPath(Config.TemplateOptions.WithSecrets)
100+
101+
// Marshal the bundle
102+
data, err := yaml.Marshal(bundle)
103+
if err != nil {
104+
return fmt.Errorf("failed to marshal secrets: %w", err)
105+
}
106+
107+
// Save secrets.yaml
108+
if err := os.WriteFile(secretsPath, data, 0o600); err != nil {
109+
return fmt.Errorf("failed to write secrets.yaml: %w", err)
110+
}
111+
112+
// Update secrets.encrypted.yaml if it exists
113+
encryptedPath := filepath.Join(Config.RootDir, "secrets.encrypted.yaml")
114+
if fileExists(encryptedPath) {
115+
fmt.Fprintf(os.Stderr, "Updating secrets.encrypted.yaml...\n")
116+
if err := age.EncryptSecretsFile(Config.RootDir); err != nil {
117+
return fmt.Errorf("failed to encrypt secrets.yaml: %w", err)
118+
}
119+
}
120+
121+
return nil
122+
}

pkg/commands/kubeconfig_handler.go

Lines changed: 3 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import (
2020
"path/filepath"
2121
"strings"
2222

23-
"github.com/cozystack/talm/pkg/age"
2423
"github.com/spf13/cobra"
2524
)
2625

@@ -128,29 +127,9 @@ func wrapKubeconfigCommand(wrappedCmd *cobra.Command, originalRunE func(*cobra.C
128127
// Automatically update kubeconfig.encrypted if it exists and talm.key exists
129128
// Skip this if --login flag is set
130129
if !loginFlagValue {
131-
// Get relative path from project root for encryption
132-
rootAbs, err := filepath.Abs(Config.RootDir)
133-
if err == nil {
134-
relKubeconfigPath, err := filepath.Rel(rootAbs, absPath)
135-
if err == nil && !strings.HasPrefix(relKubeconfigPath, "..") {
136-
// Path is within project root
137-
encryptedKubeconfigPath := relKubeconfigPath + ".encrypted"
138-
encryptedKubeconfigFile := filepath.Join(Config.RootDir, encryptedKubeconfigPath)
139-
keyFile := filepath.Join(Config.RootDir, "talm.key")
140-
141-
encryptedExists := fileExists(encryptedKubeconfigFile)
142-
keyExists := fileExists(keyFile)
143-
144-
if encryptedExists && keyExists {
145-
// Both files exist, encrypt kubeconfig
146-
if err := age.EncryptYAMLFile(Config.RootDir, relKubeconfigPath, encryptedKubeconfigPath); err != nil {
147-
// Don't fail the command if encryption fails, but log warning
148-
fmt.Fprintf(os.Stderr, "Warning: failed to encrypt kubeconfig: %v\n", err)
149-
} else {
150-
fmt.Fprintf(os.Stderr, "Updated %s\n", encryptedKubeconfigPath)
151-
}
152-
}
153-
}
130+
if err := UpdateKubeconfigEncryption(absPath); err != nil {
131+
// Don't fail the command if encryption fails, but log warning
132+
fmt.Fprintf(os.Stderr, "Warning: %v\n", err)
154133
}
155134
}
156135
}

0 commit comments

Comments
 (0)