m/c/metroctl: add k8s credentials plugin
This adds a command implementing the K8s client-go credentials
interface. It provides Metropolis credentials to Kubernetes clients
like kubectl for use with an authenticating proxy being added later.
Change-Id: I11d29f80134c2ec0839f0619eaebc4a4bb2aa3e0
Reviewed-on: https://review.monogon.dev/c/monogon/+/508
Reviewed-by: Sergiusz Bazanski <serge@monogon.tech>
diff --git a/metropolis/cli/metroctl/BUILD.bazel b/metropolis/cli/metroctl/BUILD.bazel
index b73b418..de0278b 100644
--- a/metropolis/cli/metroctl/BUILD.bazel
+++ b/metropolis/cli/metroctl/BUILD.bazel
@@ -3,7 +3,9 @@
go_library(
name = "go_default_library",
srcs = [
+ "credentials.go",
"install.go",
+ "k8scredplugin.go",
"main.go",
"takeownership.go",
],
@@ -22,6 +24,8 @@
"//metropolis/proto/api:go_default_library",
"@com_github_adrg_xdg//:go_default_library",
"@com_github_spf13_cobra//:go_default_library",
+ "@io_k8s_apimachinery//pkg/apis/meta/v1:go_default_library",
+ "@io_k8s_client_go//pkg/apis/clientauthentication/v1beta1:go_default_library",
],
)
diff --git a/metropolis/cli/metroctl/credentials.go b/metropolis/cli/metroctl/credentials.go
new file mode 100644
index 0000000..5c23f50
--- /dev/null
+++ b/metropolis/cli/metroctl/credentials.go
@@ -0,0 +1,55 @@
+package main
+
+import (
+ "crypto/ed25519"
+ "crypto/x509"
+ "encoding/pem"
+ "errors"
+ "fmt"
+ "os"
+ "path/filepath"
+
+ "github.com/adrg/xdg"
+)
+
+var noCredentialsError = errors.New("owner certificate or key does not exist")
+
+// getCredentials returns Metropolis credentials (if any) from the current
+// metroctl config directory.
+func getCredentials() (cert *x509.Certificate, key ed25519.PrivateKey, err error) {
+ ownerPrivateKeyPEM, err := os.ReadFile(filepath.Join(xdg.ConfigHome, "metroctl/owner-key.pem"))
+ if os.IsNotExist(err) {
+ return nil, nil, noCredentialsError
+ } else if err != nil {
+ return nil, nil, fmt.Errorf("failed to load owner private key: %w", err)
+ }
+ block, _ := pem.Decode(ownerPrivateKeyPEM)
+ if block == nil {
+ return nil, nil, errors.New("owner-key.pem contains invalid PEM armoring")
+ }
+ if block.Type != ownerKeyType {
+ return nil, nil, fmt.Errorf("owner-key.pem contains a PEM block that's not a %v", ownerKeyType)
+ }
+ if len(block.Bytes) != ed25519.PrivateKeySize {
+ return nil, nil, errors.New("owner-key.pem contains a non-Ed25519 key")
+ }
+ key = block.Bytes
+ ownerCertPEM, err := os.ReadFile(filepath.Join(xdg.ConfigHome, "metroctl/owner.pem"))
+ if os.IsNotExist(err) {
+ return nil, nil, noCredentialsError
+ } else if err != nil {
+ return nil, nil, fmt.Errorf("failed to load owner certificate: %w", err)
+ }
+ block, _ = pem.Decode(ownerCertPEM)
+ if block == nil {
+ return nil, nil, errors.New("owner.pem contains invalid PEM armoring")
+ }
+ if block.Type != "CERTIFICATE" {
+ return nil, nil, fmt.Errorf("owner.pem contains a PEM block that's not a CERTIFICATE")
+ }
+ cert, err = x509.ParseCertificate(block.Bytes)
+ if err != nil {
+ return nil, nil, fmt.Errorf("owner.pem contains an invalid X.509 certificate: %w", err)
+ }
+ return
+}
diff --git a/metropolis/cli/metroctl/k8scredplugin.go b/metropolis/cli/metroctl/k8scredplugin.go
new file mode 100644
index 0000000..d2e591a
--- /dev/null
+++ b/metropolis/cli/metroctl/k8scredplugin.go
@@ -0,0 +1,57 @@
+package main
+
+import (
+ "crypto/x509"
+ "encoding/json"
+ "encoding/pem"
+ "log"
+ "os"
+
+ "github.com/spf13/cobra"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ clientauthentication "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
+)
+
+var k8scredpluginCmd = &cobra.Command{
+ Use: "k8scredplugin",
+ Short: "Kubernetes client-go credential plugin [internal use]",
+ Long: `This implements a Kubernetes client-go credential plugin to
+authenticate client-go based callers including kubectl against a Metropolis
+cluster. This should never be directly called by end users.`,
+ Args: cobra.ExactArgs(0),
+ Run: doK8sCredPlugin,
+}
+
+func doK8sCredPlugin(cmd *cobra.Command, args []string) {
+ cert, key, err := getCredentials()
+ if err == noCredentialsError {
+ log.Fatal("No credentials found on your machine")
+ }
+ if err != nil {
+ log.Fatalf("failed to get Metropolis credentials: %v", err)
+ }
+
+ pkcs8Key, err := x509.MarshalPKCS8PrivateKey(key)
+ if err != nil {
+ // We explicitly pass an Ed25519 private key in, so this can't happen
+ panic(err)
+ }
+
+ cred := clientauthentication.ExecCredential{
+ TypeMeta: metav1.TypeMeta{
+ APIVersion: clientauthentication.SchemeGroupVersion.String(),
+ Kind: "ExecCredential",
+ },
+ Status: &clientauthentication.ExecCredentialStatus{
+ ClientCertificateData: string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw})),
+ ClientKeyData: string(pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: pkcs8Key})),
+ },
+ }
+ if err := json.NewEncoder(os.Stdout).Encode(cred); err != nil {
+ log.Fatalf("failed to encode ExecCredential: %v", err)
+ }
+}
+
+func init() {
+ rootCmd.AddCommand(k8scredpluginCmd)
+}