blob: 92a8871a0a1d66df6588c2e37f0486b69318dffd [file] [log] [blame]
Serge Bazanskicf23ebc2023-03-14 17:02:04 +01001package core
2
3import (
4 "crypto/ed25519"
5 "crypto/rand"
Serge Bazanski7eeef0f2024-02-05 14:40:15 +01006 "crypto/tls"
Serge Bazanskicf23ebc2023-03-14 17:02:04 +01007 "crypto/x509"
8 "encoding/pem"
9 "errors"
10 "fmt"
Serge Bazanski1f8cad72023-03-20 16:58:10 +010011 "net"
12 "net/url"
Serge Bazanskicf23ebc2023-03-14 17:02:04 +010013 "os"
14 "path/filepath"
15
16 clientauthentication "k8s.io/client-go/pkg/apis/clientauthentication/v1"
17 "k8s.io/client-go/tools/clientcmd"
18 clientapi "k8s.io/client-go/tools/clientcmd/api"
Serge Bazanski1f8cad72023-03-20 16:58:10 +010019
20 "source.monogon.dev/metropolis/node"
Serge Bazanskicf23ebc2023-03-14 17:02:04 +010021)
22
23const (
24 // OwnerKeyFileName is the filename of the owner key in a metroctl config
25 // directory.
26 OwnerKeyFileName = "owner-key.pem"
27 // OwnerCertificateFileName is the filename of the owner certificate in a
28 // metroctl config directory.
29 OwnerCertificateFileName = "owner.pem"
Serge Bazanski7eeef0f2024-02-05 14:40:15 +010030 // CACertificateFileName is the filename of the cluster CA certificate in a
31 // metroctl config directory.
32 CACertificateFileName = "ca.pem"
Serge Bazanskicf23ebc2023-03-14 17:02:04 +010033)
34
35// NoCredentialsError indicates that the requested datum (eg. owner key or owner
36// certificate) is not present in the requested directory.
37var NoCredentialsError = errors.New("owner certificate or key does not exist")
38
Serge Bazanski7eeef0f2024-02-05 14:40:15 +010039var NoCACertificateError = errors.New("no cluster CA certificate while secure connection was requested")
40
Serge Bazanskicf23ebc2023-03-14 17:02:04 +010041// A PEM block type for a Metropolis initial owner private key
42const ownerKeyType = "METROPOLIS INITIAL OWNER PRIVATE KEY"
43
44// GetOrMakeOwnerKey returns the owner key for a given metroctl configuration
45// directory path, generating and saving it first if it doesn't exist.
46func GetOrMakeOwnerKey(path string) (ed25519.PrivateKey, error) {
47 existing, err := GetOwnerKey(path)
48 switch err {
49 case nil:
50 return existing, nil
51 case NoCredentialsError:
52 default:
53 return nil, err
54 }
55
56 _, priv, err := ed25519.GenerateKey(rand.Reader)
57 if err != nil {
58 return nil, fmt.Errorf("when generating key: %w", err)
59 }
60 if err := WriteOwnerKey(path, priv); err != nil {
61 return nil, err
62 }
63 return priv, nil
64}
65
66// WriteOwnerKey saves a given raw ED25519 private key as the owner key at a
67// given metroctl configuration directory path.
68func WriteOwnerKey(path string, priv ed25519.PrivateKey) error {
69 pemPriv := pem.EncodeToMemory(&pem.Block{Type: ownerKeyType, Bytes: priv})
70 if err := os.WriteFile(filepath.Join(path, OwnerKeyFileName), pemPriv, 0600); err != nil {
71 return fmt.Errorf("when saving key: %w", err)
72 }
73 return nil
74}
75
Serge Bazanski7eeef0f2024-02-05 14:40:15 +010076// WriteCACertificate writes the given der-encoded X509 certificate to the given
77// metorctl configuration directory path.
78func WriteCACertificate(path string, der []byte) error {
79 pemCert := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})
80 if err := os.WriteFile(filepath.Join(path, CACertificateFileName), pemCert, 0600); err != nil {
81 return fmt.Errorf("when saving CA certificate: %w", err)
82 }
83 return nil
84}
85
Serge Bazanskicf23ebc2023-03-14 17:02:04 +010086// GetOwnerKey loads and returns a raw ED25519 private key from the saved owner
87// key in a given metroctl configuration directory path. If the owner key doesn't
88// exist, NoCredentialsError will be returned.
89func GetOwnerKey(path string) (ed25519.PrivateKey, error) {
90 ownerPrivateKeyPEM, err := os.ReadFile(filepath.Join(path, OwnerKeyFileName))
91 if os.IsNotExist(err) {
92 return nil, NoCredentialsError
93 } else if err != nil {
94 return nil, fmt.Errorf("failed to load owner private key: %w", err)
95 }
96 block, _ := pem.Decode(ownerPrivateKeyPEM)
97 if block == nil {
98 return nil, errors.New("owner-key.pem contains invalid PEM armoring")
99 }
100 if block.Type != ownerKeyType {
101 return nil, fmt.Errorf("owner-key.pem contains a PEM block that's not a %v", ownerKeyType)
102 }
103 if len(block.Bytes) != ed25519.PrivateKeySize {
104 return nil, errors.New("owner-key.pem contains a non-Ed25519 key")
105 }
106 return block.Bytes, nil
107}
108
109// WriteOwnerCertificate saves a given DER-encoded X509 certificate as the owner
110// key for a given metroctl configuration directory path.
111func WriteOwnerCertificate(path string, cert []byte) error {
112 ownerCertPEM := pem.Block{
113 Type: "CERTIFICATE",
114 Bytes: cert,
115 }
116 if err := os.WriteFile(filepath.Join(path, OwnerCertificateFileName), pem.EncodeToMemory(&ownerCertPEM), 0644); err != nil {
117 return err
118 }
119 return nil
120}
121
122// GetOwnerCredentials loads and returns a raw ED25519 private key alongside a
123// DER-encoded X509 certificate from the saved owner key and certificate in a
124// given metroctl configuration directory path. If either the key or certificate
125// doesn't exist, NoCredentialsError will be returned.
126func GetOwnerCredentials(path string) (cert *x509.Certificate, key ed25519.PrivateKey, err error) {
127 key, err = GetOwnerKey(path)
128 if err != nil {
129 return nil, nil, err
130 }
131
132 ownerCertPEM, err := os.ReadFile(filepath.Join(path, OwnerCertificateFileName))
133 if os.IsNotExist(err) {
134 return nil, nil, NoCredentialsError
135 } else if err != nil {
136 return nil, nil, fmt.Errorf("failed to load owner certificate: %w", err)
137 }
138 block, _ := pem.Decode(ownerCertPEM)
139 if block == nil {
140 return nil, nil, errors.New("owner.pem contains invalid PEM armoring")
141 }
142 if block.Type != "CERTIFICATE" {
143 return nil, nil, fmt.Errorf("owner.pem contains a PEM block that's not a CERTIFICATE")
144 }
145 cert, err = x509.ParseCertificate(block.Bytes)
146 if err != nil {
147 return nil, nil, fmt.Errorf("owner.pem contains an invalid X.509 certificate: %w", err)
148 }
149 return
150}
151
Serge Bazanski7eeef0f2024-02-05 14:40:15 +0100152// GetOwnerTLSCredentials returns a client TLS Certificate for authenticating to
153// the metropolis cluster, based on metroctl configuration at a given path.
154func GetOwnerTLSCredentials(path string) (*tls.Certificate, error) {
155 ocert, opkey, err := GetOwnerCredentials(path)
156 if err != nil {
157 return nil, err
158 }
159 return &tls.Certificate{
160 Certificate: [][]byte{ocert.Raw},
161 PrivateKey: opkey,
162 }, nil
163}
164
165// GetClusterCA returns the saved cluster CA certificate at the given metoctl
166// configuration path. This does not perform TOFU if the certificate is not
167// present.
168func GetClusterCA(path string) (cert *x509.Certificate, err error) {
169 caCertPEM, err := os.ReadFile(filepath.Join(path, CACertificateFileName))
170 if os.IsNotExist(err) {
171 return nil, NoCACertificateError
172 } else if err != nil {
173 return nil, fmt.Errorf("failed to load CA certificate: %w", err)
174 }
175 block, _ := pem.Decode(caCertPEM)
176 if block == nil {
177 return nil, errors.New("ca.pem contains invalid PEM armoring")
178 }
179 if block.Type != "CERTIFICATE" {
180 return nil, fmt.Errorf("ca.pem contains a PEM block that's not a CERTIFICATE")
181 }
182 cert, err = x509.ParseCertificate(block.Bytes)
183 if err != nil {
184 return nil, fmt.Errorf("ca.pem contains an invalid X.509 certificate: %w", err)
185 }
186 return
187}
188
Serge Bazanski1f8cad72023-03-20 16:58:10 +0100189// InstallKubeletConfig modifies the default kubelet kubeconfig of the host
190// system to be able to connect via a metroctl (and an associated ConnectOptions)
191// to a Kubernetes apiserver at IP address/hostname 'server'.
192//
193// The kubelet's kubeconfig changes will be limited to contexts/configs/... named
194// configName. The configName context will be made the default context only if
195// there is no other default context in the current subconfig.
196//
197// Kubeconfigs can only take a single Kubernetes server address, so this function
198// similarly only allows you to specify only a single server address.
199func InstallKubeletConfig(metroctlPath string, opts *ConnectOptions, configName, server string) error {
Serge Bazanskicf23ebc2023-03-14 17:02:04 +0100200 ca := clientcmd.NewDefaultPathOptions()
201 config, err := ca.GetStartingConfig()
202 if err != nil {
203 return fmt.Errorf("getting initial config failed: %w", err)
204 }
205
Serge Bazanski1f8cad72023-03-20 16:58:10 +0100206 args := []string{
207 "k8scredplugin",
208 }
209 args = append(args, opts.ToFlags()...)
210
Serge Bazanskicf23ebc2023-03-14 17:02:04 +0100211 config.AuthInfos[configName] = &clientapi.AuthInfo{
212 Exec: &clientapi.ExecConfig{
213 APIVersion: clientauthentication.SchemeGroupVersion.String(),
214 Command: metroctlPath,
Serge Bazanski1f8cad72023-03-20 16:58:10 +0100215 Args: args,
Serge Bazanskicf23ebc2023-03-14 17:02:04 +0100216 InstallHint: `Authenticating to Metropolis clusters requires metroctl to be present.
217Running metroctl takeownership creates this entry and either points to metroctl as a command in
218PATH if metroctl is in PATH at that time or to the absolute path to metroctl at that time.
219If you moved metroctl afterwards or want to switch to PATH resolution, edit $HOME/.kube/config and
220change users.metropolis.exec.command to the required path (or just metroctl if using PATH resolution).`,
221 InteractiveMode: clientapi.NeverExecInteractiveMode,
222 },
223 }
224
Serge Bazanski1f8cad72023-03-20 16:58:10 +0100225 var u url.URL
226 u.Scheme = "https"
227 u.Host = net.JoinHostPort(server, node.KubernetesAPIWrappedPort.PortString())
Serge Bazanski7eeef0f2024-02-05 14:40:15 +0100228
Serge Bazanskicf23ebc2023-03-14 17:02:04 +0100229 config.Clusters[configName] = &clientapi.Cluster{
230 // MVP: This is insecure, but making this work would be wasted effort
231 // as all of it will be replaced by the identity system.
232 // TODO(issues/144): adjust cluster endpoints once have functioning roles
233 // implemented.
234 InsecureSkipTLSVerify: true,
Serge Bazanski1f8cad72023-03-20 16:58:10 +0100235 Server: u.String(),
236 ProxyURL: opts.ProxyURL(),
Serge Bazanskicf23ebc2023-03-14 17:02:04 +0100237 }
238
239 config.Contexts[configName] = &clientapi.Context{
240 AuthInfo: configName,
241 Cluster: configName,
242 Namespace: "default",
243 }
244
245 // Only set us as the current context if no other exists. Changing that
246 // unprompted would be kind of rude.
247 if config.CurrentContext == "" {
248 config.CurrentContext = configName
249 }
250
251 if err := clientcmd.ModifyConfig(ca, *config, true); err != nil {
252 return fmt.Errorf("modifying config failed: %w", err)
253 }
254 return nil
255}
Serge Bazanski1f8cad72023-03-20 16:58:10 +0100256
257// ConnectOptions define how to reach a Metropolis cluster from metroctl.
258//
259// This structure can be built directly. All unset fields mean 'default'. It can
260// then be used to generate the equivalent flags to passs to metroctl.
261//
262// Nil pointers to ConnectOptions are equivalent to an empty ConneectOptions when
263// methods on it are called.
264type ConnectOptions struct {
265 // ConfigPath is the path at which the metroctl configuration/credentials live.
266 // If not set, the default will be used.
267 ConfigPath string
268 // ProxyServer is a host:port pair that indicates the metropolis cluster should
269 // be reached via the given SOCKS5 proxy. If not set, the cluster can be reached
270 // directly from the host networking stack.
271 ProxyServer string
272 // Endpoints are the IP addresses/hostnames (without port part) of the Metropolis
273 // instances that metroctl should use to establish connectivity to a cluster.
274 // These instances should have the ControlPlane role set.
275 Endpoints []string
Serge Bazanski925ec3d2024-02-05 14:38:20 +0100276 // ResolverLogger can be set to enable verbose logging of the Metropolis RPC
277 // resolver layer.
278 ResolverLogger ResolverLogger
Serge Bazanski7eeef0f2024-02-05 14:40:15 +0100279 // TOFU overrides the trust-on-first-use behaviour for CA certificates for the
280 // connection. If not set, TerminalTOFU is used which will interactively ask the
281 // user to accept a CA certificate using os.Stdin/Stdout.
282 TOFU CertificateTOFU
Serge Bazanski1f8cad72023-03-20 16:58:10 +0100283}
284
285// ToFlags returns the metroctl flags corresponding to the options described by
286// this ConnectionOptions struct.
287func (c *ConnectOptions) ToFlags() []string {
288 var res []string
289
290 if c == nil {
291 return res
292 }
293
294 if c.ConfigPath != "" {
295 res = append(res, "--config", c.ConfigPath)
296 }
297 if c.ProxyServer != "" {
298 res = append(res, "--proxy", c.ProxyServer)
299 }
300 for _, ep := range c.Endpoints {
301 res = append(res, "--endpoints", ep)
302 }
303
304 return res
305}
306
307// ProxyURL returns a kubeconfig-compatible URL of the proxy server configured by
308// ConnectOptions, or an empty string if not set.
309func (c *ConnectOptions) ProxyURL() string {
310 if c == nil {
311 return ""
312 }
313 if c.ProxyServer == "" {
314 return ""
315 }
316 var u url.URL
317 u.Scheme = "socks5"
318 u.Host = c.ProxyServer
319 return u.String()
320}