blob: 971442117eba2f92adb9ac857fa3c0f2e5269001 [file] [log] [blame]
Serge Bazanskicf23ebc2023-03-14 17:02:04 +01001package core
2
3import (
4 "crypto/ed25519"
5 "crypto/rand"
6 "crypto/x509"
7 "encoding/pem"
8 "errors"
9 "fmt"
Serge Bazanski1f8cad72023-03-20 16:58:10 +010010 "net"
11 "net/url"
Serge Bazanskicf23ebc2023-03-14 17:02:04 +010012 "os"
13 "path/filepath"
14
15 clientauthentication "k8s.io/client-go/pkg/apis/clientauthentication/v1"
16 "k8s.io/client-go/tools/clientcmd"
17 clientapi "k8s.io/client-go/tools/clientcmd/api"
Serge Bazanski1f8cad72023-03-20 16:58:10 +010018
19 "source.monogon.dev/metropolis/node"
Serge Bazanskicf23ebc2023-03-14 17:02:04 +010020)
21
22const (
23 // OwnerKeyFileName is the filename of the owner key in a metroctl config
24 // directory.
25 OwnerKeyFileName = "owner-key.pem"
26 // OwnerCertificateFileName is the filename of the owner certificate in a
27 // metroctl config directory.
28 OwnerCertificateFileName = "owner.pem"
29)
30
31// NoCredentialsError indicates that the requested datum (eg. owner key or owner
32// certificate) is not present in the requested directory.
33var NoCredentialsError = errors.New("owner certificate or key does not exist")
34
35// A PEM block type for a Metropolis initial owner private key
36const ownerKeyType = "METROPOLIS INITIAL OWNER PRIVATE KEY"
37
38// GetOrMakeOwnerKey returns the owner key for a given metroctl configuration
39// directory path, generating and saving it first if it doesn't exist.
40func GetOrMakeOwnerKey(path string) (ed25519.PrivateKey, error) {
41 existing, err := GetOwnerKey(path)
42 switch err {
43 case nil:
44 return existing, nil
45 case NoCredentialsError:
46 default:
47 return nil, err
48 }
49
50 _, priv, err := ed25519.GenerateKey(rand.Reader)
51 if err != nil {
52 return nil, fmt.Errorf("when generating key: %w", err)
53 }
54 if err := WriteOwnerKey(path, priv); err != nil {
55 return nil, err
56 }
57 return priv, nil
58}
59
60// WriteOwnerKey saves a given raw ED25519 private key as the owner key at a
61// given metroctl configuration directory path.
62func WriteOwnerKey(path string, priv ed25519.PrivateKey) error {
63 pemPriv := pem.EncodeToMemory(&pem.Block{Type: ownerKeyType, Bytes: priv})
64 if err := os.WriteFile(filepath.Join(path, OwnerKeyFileName), pemPriv, 0600); err != nil {
65 return fmt.Errorf("when saving key: %w", err)
66 }
67 return nil
68}
69
70// GetOwnerKey loads and returns a raw ED25519 private key from the saved owner
71// key in a given metroctl configuration directory path. If the owner key doesn't
72// exist, NoCredentialsError will be returned.
73func GetOwnerKey(path string) (ed25519.PrivateKey, error) {
74 ownerPrivateKeyPEM, err := os.ReadFile(filepath.Join(path, OwnerKeyFileName))
75 if os.IsNotExist(err) {
76 return nil, NoCredentialsError
77 } else if err != nil {
78 return nil, fmt.Errorf("failed to load owner private key: %w", err)
79 }
80 block, _ := pem.Decode(ownerPrivateKeyPEM)
81 if block == nil {
82 return nil, errors.New("owner-key.pem contains invalid PEM armoring")
83 }
84 if block.Type != ownerKeyType {
85 return nil, fmt.Errorf("owner-key.pem contains a PEM block that's not a %v", ownerKeyType)
86 }
87 if len(block.Bytes) != ed25519.PrivateKeySize {
88 return nil, errors.New("owner-key.pem contains a non-Ed25519 key")
89 }
90 return block.Bytes, nil
91}
92
93// WriteOwnerCertificate saves a given DER-encoded X509 certificate as the owner
94// key for a given metroctl configuration directory path.
95func WriteOwnerCertificate(path string, cert []byte) error {
96 ownerCertPEM := pem.Block{
97 Type: "CERTIFICATE",
98 Bytes: cert,
99 }
100 if err := os.WriteFile(filepath.Join(path, OwnerCertificateFileName), pem.EncodeToMemory(&ownerCertPEM), 0644); err != nil {
101 return err
102 }
103 return nil
104}
105
106// GetOwnerCredentials loads and returns a raw ED25519 private key alongside a
107// DER-encoded X509 certificate from the saved owner key and certificate in a
108// given metroctl configuration directory path. If either the key or certificate
109// doesn't exist, NoCredentialsError will be returned.
110func GetOwnerCredentials(path string) (cert *x509.Certificate, key ed25519.PrivateKey, err error) {
111 key, err = GetOwnerKey(path)
112 if err != nil {
113 return nil, nil, err
114 }
115
116 ownerCertPEM, err := os.ReadFile(filepath.Join(path, OwnerCertificateFileName))
117 if os.IsNotExist(err) {
118 return nil, nil, NoCredentialsError
119 } else if err != nil {
120 return nil, nil, fmt.Errorf("failed to load owner certificate: %w", err)
121 }
122 block, _ := pem.Decode(ownerCertPEM)
123 if block == nil {
124 return nil, nil, errors.New("owner.pem contains invalid PEM armoring")
125 }
126 if block.Type != "CERTIFICATE" {
127 return nil, nil, fmt.Errorf("owner.pem contains a PEM block that's not a CERTIFICATE")
128 }
129 cert, err = x509.ParseCertificate(block.Bytes)
130 if err != nil {
131 return nil, nil, fmt.Errorf("owner.pem contains an invalid X.509 certificate: %w", err)
132 }
133 return
134}
135
Serge Bazanski1f8cad72023-03-20 16:58:10 +0100136// InstallKubeletConfig modifies the default kubelet kubeconfig of the host
137// system to be able to connect via a metroctl (and an associated ConnectOptions)
138// to a Kubernetes apiserver at IP address/hostname 'server'.
139//
140// The kubelet's kubeconfig changes will be limited to contexts/configs/... named
141// configName. The configName context will be made the default context only if
142// there is no other default context in the current subconfig.
143//
144// Kubeconfigs can only take a single Kubernetes server address, so this function
145// similarly only allows you to specify only a single server address.
146func InstallKubeletConfig(metroctlPath string, opts *ConnectOptions, configName, server string) error {
Serge Bazanskicf23ebc2023-03-14 17:02:04 +0100147 ca := clientcmd.NewDefaultPathOptions()
148 config, err := ca.GetStartingConfig()
149 if err != nil {
150 return fmt.Errorf("getting initial config failed: %w", err)
151 }
152
Serge Bazanski1f8cad72023-03-20 16:58:10 +0100153 args := []string{
154 "k8scredplugin",
155 }
156 args = append(args, opts.ToFlags()...)
157
Serge Bazanskicf23ebc2023-03-14 17:02:04 +0100158 config.AuthInfos[configName] = &clientapi.AuthInfo{
159 Exec: &clientapi.ExecConfig{
160 APIVersion: clientauthentication.SchemeGroupVersion.String(),
161 Command: metroctlPath,
Serge Bazanski1f8cad72023-03-20 16:58:10 +0100162 Args: args,
Serge Bazanskicf23ebc2023-03-14 17:02:04 +0100163 InstallHint: `Authenticating to Metropolis clusters requires metroctl to be present.
164Running metroctl takeownership creates this entry and either points to metroctl as a command in
165PATH if metroctl is in PATH at that time or to the absolute path to metroctl at that time.
166If you moved metroctl afterwards or want to switch to PATH resolution, edit $HOME/.kube/config and
167change users.metropolis.exec.command to the required path (or just metroctl if using PATH resolution).`,
168 InteractiveMode: clientapi.NeverExecInteractiveMode,
169 },
170 }
171
Serge Bazanski1f8cad72023-03-20 16:58:10 +0100172 var u url.URL
173 u.Scheme = "https"
174 u.Host = net.JoinHostPort(server, node.KubernetesAPIWrappedPort.PortString())
Serge Bazanskicf23ebc2023-03-14 17:02:04 +0100175 config.Clusters[configName] = &clientapi.Cluster{
176 // MVP: This is insecure, but making this work would be wasted effort
177 // as all of it will be replaced by the identity system.
178 // TODO(issues/144): adjust cluster endpoints once have functioning roles
179 // implemented.
180 InsecureSkipTLSVerify: true,
Serge Bazanski1f8cad72023-03-20 16:58:10 +0100181 Server: u.String(),
182 ProxyURL: opts.ProxyURL(),
Serge Bazanskicf23ebc2023-03-14 17:02:04 +0100183 }
184
185 config.Contexts[configName] = &clientapi.Context{
186 AuthInfo: configName,
187 Cluster: configName,
188 Namespace: "default",
189 }
190
191 // Only set us as the current context if no other exists. Changing that
192 // unprompted would be kind of rude.
193 if config.CurrentContext == "" {
194 config.CurrentContext = configName
195 }
196
197 if err := clientcmd.ModifyConfig(ca, *config, true); err != nil {
198 return fmt.Errorf("modifying config failed: %w", err)
199 }
200 return nil
201}
Serge Bazanski1f8cad72023-03-20 16:58:10 +0100202
203// ConnectOptions define how to reach a Metropolis cluster from metroctl.
204//
205// This structure can be built directly. All unset fields mean 'default'. It can
206// then be used to generate the equivalent flags to passs to metroctl.
207//
208// Nil pointers to ConnectOptions are equivalent to an empty ConneectOptions when
209// methods on it are called.
210type ConnectOptions struct {
211 // ConfigPath is the path at which the metroctl configuration/credentials live.
212 // If not set, the default will be used.
213 ConfigPath string
214 // ProxyServer is a host:port pair that indicates the metropolis cluster should
215 // be reached via the given SOCKS5 proxy. If not set, the cluster can be reached
216 // directly from the host networking stack.
217 ProxyServer string
218 // Endpoints are the IP addresses/hostnames (without port part) of the Metropolis
219 // instances that metroctl should use to establish connectivity to a cluster.
220 // These instances should have the ControlPlane role set.
221 Endpoints []string
222}
223
224// ToFlags returns the metroctl flags corresponding to the options described by
225// this ConnectionOptions struct.
226func (c *ConnectOptions) ToFlags() []string {
227 var res []string
228
229 if c == nil {
230 return res
231 }
232
233 if c.ConfigPath != "" {
234 res = append(res, "--config", c.ConfigPath)
235 }
236 if c.ProxyServer != "" {
237 res = append(res, "--proxy", c.ProxyServer)
238 }
239 for _, ep := range c.Endpoints {
240 res = append(res, "--endpoints", ep)
241 }
242
243 return res
244}
245
246// ProxyURL returns a kubeconfig-compatible URL of the proxy server configured by
247// ConnectOptions, or an empty string if not set.
248func (c *ConnectOptions) ProxyURL() string {
249 if c == nil {
250 return ""
251 }
252 if c.ProxyServer == "" {
253 return ""
254 }
255 var u url.URL
256 u.Scheme = "socks5"
257 u.Host = c.ProxyServer
258 return u.String()
259}