blob: d62bf2e0df7d2fb192d864ad0fa888dd87974108 [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"
10 "os"
11 "path/filepath"
12
13 clientauthentication "k8s.io/client-go/pkg/apis/clientauthentication/v1"
14 "k8s.io/client-go/tools/clientcmd"
15 clientapi "k8s.io/client-go/tools/clientcmd/api"
16)
17
18const (
19 // OwnerKeyFileName is the filename of the owner key in a metroctl config
20 // directory.
21 OwnerKeyFileName = "owner-key.pem"
22 // OwnerCertificateFileName is the filename of the owner certificate in a
23 // metroctl config directory.
24 OwnerCertificateFileName = "owner.pem"
25)
26
27// NoCredentialsError indicates that the requested datum (eg. owner key or owner
28// certificate) is not present in the requested directory.
29var NoCredentialsError = errors.New("owner certificate or key does not exist")
30
31// A PEM block type for a Metropolis initial owner private key
32const ownerKeyType = "METROPOLIS INITIAL OWNER PRIVATE KEY"
33
34// GetOrMakeOwnerKey returns the owner key for a given metroctl configuration
35// directory path, generating and saving it first if it doesn't exist.
36func GetOrMakeOwnerKey(path string) (ed25519.PrivateKey, error) {
37 existing, err := GetOwnerKey(path)
38 switch err {
39 case nil:
40 return existing, nil
41 case NoCredentialsError:
42 default:
43 return nil, err
44 }
45
46 _, priv, err := ed25519.GenerateKey(rand.Reader)
47 if err != nil {
48 return nil, fmt.Errorf("when generating key: %w", err)
49 }
50 if err := WriteOwnerKey(path, priv); err != nil {
51 return nil, err
52 }
53 return priv, nil
54}
55
56// WriteOwnerKey saves a given raw ED25519 private key as the owner key at a
57// given metroctl configuration directory path.
58func WriteOwnerKey(path string, priv ed25519.PrivateKey) error {
59 pemPriv := pem.EncodeToMemory(&pem.Block{Type: ownerKeyType, Bytes: priv})
60 if err := os.WriteFile(filepath.Join(path, OwnerKeyFileName), pemPriv, 0600); err != nil {
61 return fmt.Errorf("when saving key: %w", err)
62 }
63 return nil
64}
65
66// GetOwnerKey loads and returns a raw ED25519 private key from the saved owner
67// key in a given metroctl configuration directory path. If the owner key doesn't
68// exist, NoCredentialsError will be returned.
69func GetOwnerKey(path string) (ed25519.PrivateKey, error) {
70 ownerPrivateKeyPEM, err := os.ReadFile(filepath.Join(path, OwnerKeyFileName))
71 if os.IsNotExist(err) {
72 return nil, NoCredentialsError
73 } else if err != nil {
74 return nil, fmt.Errorf("failed to load owner private key: %w", err)
75 }
76 block, _ := pem.Decode(ownerPrivateKeyPEM)
77 if block == nil {
78 return nil, errors.New("owner-key.pem contains invalid PEM armoring")
79 }
80 if block.Type != ownerKeyType {
81 return nil, fmt.Errorf("owner-key.pem contains a PEM block that's not a %v", ownerKeyType)
82 }
83 if len(block.Bytes) != ed25519.PrivateKeySize {
84 return nil, errors.New("owner-key.pem contains a non-Ed25519 key")
85 }
86 return block.Bytes, nil
87}
88
89// WriteOwnerCertificate saves a given DER-encoded X509 certificate as the owner
90// key for a given metroctl configuration directory path.
91func WriteOwnerCertificate(path string, cert []byte) error {
92 ownerCertPEM := pem.Block{
93 Type: "CERTIFICATE",
94 Bytes: cert,
95 }
96 if err := os.WriteFile(filepath.Join(path, OwnerCertificateFileName), pem.EncodeToMemory(&ownerCertPEM), 0644); err != nil {
97 return err
98 }
99 return nil
100}
101
102// GetOwnerCredentials loads and returns a raw ED25519 private key alongside a
103// DER-encoded X509 certificate from the saved owner key and certificate in a
104// given metroctl configuration directory path. If either the key or certificate
105// doesn't exist, NoCredentialsError will be returned.
106func GetOwnerCredentials(path string) (cert *x509.Certificate, key ed25519.PrivateKey, err error) {
107 key, err = GetOwnerKey(path)
108 if err != nil {
109 return nil, nil, err
110 }
111
112 ownerCertPEM, err := os.ReadFile(filepath.Join(path, OwnerCertificateFileName))
113 if os.IsNotExist(err) {
114 return nil, nil, NoCredentialsError
115 } else if err != nil {
116 return nil, nil, fmt.Errorf("failed to load owner certificate: %w", err)
117 }
118 block, _ := pem.Decode(ownerCertPEM)
119 if block == nil {
120 return nil, nil, errors.New("owner.pem contains invalid PEM armoring")
121 }
122 if block.Type != "CERTIFICATE" {
123 return nil, nil, fmt.Errorf("owner.pem contains a PEM block that's not a CERTIFICATE")
124 }
125 cert, err = x509.ParseCertificate(block.Bytes)
126 if err != nil {
127 return nil, nil, fmt.Errorf("owner.pem contains an invalid X.509 certificate: %w", err)
128 }
129 return
130}
131
132// InstallK8SWrapper configures the current user's kubectl to connect to a
133// Kubernetes cluster as defined by server (Metropolis wrapped APIServer
134// endpoint), proxyURL (optional proxy URL) and metroctlPath (binary managing
135// credentials for this cluster, and used to implement the client-side part of
136// the Metropolis-wrapped APIServer protocol). The configuration will be saved to
137// the 'configName' context in kubectl.
138func InstallK8SWrapper(metroctlPath, configName, server, proxyURL string) error {
139 ca := clientcmd.NewDefaultPathOptions()
140 config, err := ca.GetStartingConfig()
141 if err != nil {
142 return fmt.Errorf("getting initial config failed: %w", err)
143 }
144
145 config.AuthInfos[configName] = &clientapi.AuthInfo{
146 Exec: &clientapi.ExecConfig{
147 APIVersion: clientauthentication.SchemeGroupVersion.String(),
148 Command: metroctlPath,
149 Args: []string{"k8scredplugin"},
150 InstallHint: `Authenticating to Metropolis clusters requires metroctl to be present.
151Running metroctl takeownership creates this entry and either points to metroctl as a command in
152PATH if metroctl is in PATH at that time or to the absolute path to metroctl at that time.
153If you moved metroctl afterwards or want to switch to PATH resolution, edit $HOME/.kube/config and
154change users.metropolis.exec.command to the required path (or just metroctl if using PATH resolution).`,
155 InteractiveMode: clientapi.NeverExecInteractiveMode,
156 },
157 }
158
159 config.Clusters[configName] = &clientapi.Cluster{
160 // MVP: This is insecure, but making this work would be wasted effort
161 // as all of it will be replaced by the identity system.
162 // TODO(issues/144): adjust cluster endpoints once have functioning roles
163 // implemented.
164 InsecureSkipTLSVerify: true,
165 Server: server,
166 ProxyURL: proxyURL,
167 }
168
169 config.Contexts[configName] = &clientapi.Context{
170 AuthInfo: configName,
171 Cluster: configName,
172 Namespace: "default",
173 }
174
175 // Only set us as the current context if no other exists. Changing that
176 // unprompted would be kind of rude.
177 if config.CurrentContext == "" {
178 config.CurrentContext = configName
179 }
180
181 if err := clientcmd.ModifyConfig(ca, *config, true); err != nil {
182 return fmt.Errorf("modifying config failed: %w", err)
183 }
184 return nil
185}