cloud: init with apigw
This adds a first component to the cloud project, the apigw (API
Gateway), which listens on a public gRPC-Web socket.
It's not truly a gateway - it will actually contain most of the
IAM/Project logic for the cloud system. A better name should be picked
later.
We implement a minimum internal/public gRPC(-Web) listener and some
boilerplate for the parts that are gonna pop up again. Notably, we add
some automation around generating developer TLS certificates for the
internal gRPC listener.
Currently the apigw serves a single, demo RPC which returns
'unimplemented'.
Change-Id: I9164ddbd9a20172154ae5a3ffad676de5fe4927d
Reviewed-on: https://review.monogon.dev/c/monogon/+/906
Reviewed-by: Leopold Schabel <leo@monogon.tech>
Tested-by: Jenkins CI
diff --git a/cloud/api/BUILD.bazel b/cloud/api/BUILD.bazel
new file mode 100644
index 0000000..363713f
--- /dev/null
+++ b/cloud/api/BUILD.bazel
@@ -0,0 +1,24 @@
+load("@rules_proto//proto:defs.bzl", "proto_library")
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library")
+
+proto_library(
+ name = "api_proto",
+ srcs = ["iam.proto"],
+ visibility = ["//visibility:public"],
+)
+
+go_proto_library(
+ name = "api_go_proto",
+ compilers = ["@io_bazel_rules_go//proto:go_grpc"],
+ importpath = "source.monogon.dev/cloud/api",
+ proto = ":api_proto",
+ visibility = ["//visibility:public"],
+)
+
+go_library(
+ name = "api",
+ embed = [":api_go_proto"],
+ importpath = "source.monogon.dev/cloud/api",
+ visibility = ["//visibility:public"],
+)
diff --git a/cloud/api/gomod-generated-placeholder.go b/cloud/api/gomod-generated-placeholder.go
new file mode 100644
index 0000000..778f64e
--- /dev/null
+++ b/cloud/api/gomod-generated-placeholder.go
@@ -0,0 +1 @@
+package api
diff --git a/cloud/api/iam.proto b/cloud/api/iam.proto
new file mode 100644
index 0000000..2bfc4ed
--- /dev/null
+++ b/cloud/api/iam.proto
@@ -0,0 +1,18 @@
+syntax = "proto3";
+package cloud.api;
+option go_package = "source.monogon.dev/cloud/api";
+
+service IAM {
+ rpc WhoAmI(WhoAmIRequest) returns (WhoAmIResponse);
+}
+
+message WhoAmIRequest {
+}
+
+message WhoAmIResponse {
+ // Opaque identifier (eg. UUID) of the acting account. Immutable.
+ string account_id = 1;
+ // Primary email address of the acting account. Can change, must not be used
+ // as a foreign key in other systems.
+ string email = 2;
+}
\ No newline at end of file
diff --git a/cloud/apigw/BUILD.bazel b/cloud/apigw/BUILD.bazel
new file mode 100644
index 0000000..bda0248
--- /dev/null
+++ b/cloud/apigw/BUILD.bazel
@@ -0,0 +1,15 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
+
+go_library(
+ name = "apigw_lib",
+ srcs = ["main.go"],
+ importpath = "source.monogon.dev/cloud/apigw",
+ visibility = ["//visibility:private"],
+ deps = ["//cloud/apigw/server"],
+)
+
+go_binary(
+ name = "apigw",
+ embed = [":apigw_lib"],
+ visibility = ["//visibility:public"],
+)
diff --git a/cloud/apigw/main.go b/cloud/apigw/main.go
new file mode 100644
index 0000000..d560c0c
--- /dev/null
+++ b/cloud/apigw/main.go
@@ -0,0 +1,21 @@
+package main
+
+import (
+ "context"
+ "flag"
+
+ "source.monogon.dev/cloud/apigw/server"
+)
+
+func main() {
+ s := &server.Server{}
+ s.Config.RegisterFlags()
+ flag.Parse()
+
+ ctx, ctxC := context.WithCancel(context.Background())
+ // TODO: context cancel on interrupt.
+ _ = ctxC
+
+ s.Start(ctx)
+ select {}
+}
diff --git a/cloud/apigw/server/BUILD.bazel b/cloud/apigw/server/BUILD.bazel
new file mode 100644
index 0000000..2444267
--- /dev/null
+++ b/cloud/apigw/server/BUILD.bazel
@@ -0,0 +1,31 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
+
+go_library(
+ name = "server",
+ srcs = ["server.go"],
+ importpath = "source.monogon.dev/cloud/apigw/server",
+ visibility = ["//visibility:public"],
+ deps = [
+ "//cloud/api",
+ "//cloud/lib/component",
+ "@com_github_improbable_eng_grpc_web//go/grpcweb",
+ "@io_k8s_klog_v2//:klog",
+ "@org_golang_google_grpc//:go_default_library",
+ "@org_golang_google_grpc//codes",
+ "@org_golang_google_grpc//credentials/insecure",
+ "@org_golang_google_grpc//reflection",
+ "@org_golang_google_grpc//status",
+ ],
+)
+
+go_test(
+ name = "server_test",
+ srcs = ["server_test.go"],
+ embed = [":server"],
+ deps = [
+ "//cloud/api",
+ "//cloud/lib/component",
+ "@org_golang_google_grpc//codes",
+ "@org_golang_google_protobuf//proto",
+ ],
+)
diff --git a/cloud/apigw/server/server.go b/cloud/apigw/server/server.go
new file mode 100644
index 0000000..b3b80bf
--- /dev/null
+++ b/cloud/apigw/server/server.go
@@ -0,0 +1,107 @@
+package server
+
+import (
+ "context"
+ "flag"
+ "net"
+ "net/http"
+
+ "github.com/improbable-eng/grpc-web/go/grpcweb"
+ "google.golang.org/grpc"
+ "google.golang.org/grpc/codes"
+ "google.golang.org/grpc/credentials/insecure"
+ "google.golang.org/grpc/reflection"
+ "google.golang.org/grpc/status"
+ "k8s.io/klog/v2"
+
+ apb "source.monogon.dev/cloud/api"
+ "source.monogon.dev/cloud/lib/component"
+)
+
+// Config is the main configuration of the apigw server. It's usually populated
+// from flags via RegisterFlags, but can also be set manually (eg. in tests).
+type Config struct {
+ component.Configuration
+
+ PublicListenAddress string
+}
+
+// RegisterFlags registers the component configuration to be provided by flags.
+// This must be called exactly once before then calling flags.Parse().
+func (c *Config) RegisterFlags() {
+ c.Configuration.RegisterFlags("apigw")
+ flag.StringVar(&c.PublicListenAddress, "apigw_public_grpc_listen_address", ":8080", "Address to listen at for public/user gRPC connections for apigw")
+}
+
+// Server runs the apigw server. It listens on two interfaces:
+// - Internal gRPC, which is authenticated using TLS and authorized by CA. This
+// is to be used for internal RPCs, eg. management/debug.
+// - Public gRPC-Web, which is currently unauthenticated.
+type Server struct {
+ Config Config
+
+ // ListenGRPC will contain the address at which the internal gRPC server is
+ // listening after .Start() has been called. This can differ from the configured
+ // value if the configuration requests any port (via :0).
+ ListenGRPC string
+ // ListenPublic will contain the address at which the public API server is
+ // listening after .Start() has been called. This can differ from the configured
+ // value if the configuration requests any port (via :0).
+ ListenPublic string
+}
+
+func (s *Server) startInternalGRPC(ctx context.Context) {
+ g := grpc.NewServer(s.Config.GRPCServerOptions()...)
+ lis, err := net.Listen("tcp", s.Config.GRPCListenAddress)
+ if err != nil {
+ klog.Exitf("Could not listen: %v", err)
+ }
+ s.ListenGRPC = lis.Addr().String()
+
+ reflection.Register(g)
+
+ klog.Infof("Internal gRPC listening on %s", s.ListenGRPC)
+ go func() {
+ err := g.Serve(lis)
+ if err != ctx.Err() {
+ klog.Exitf("Internal gRPC serve failed: %v", err)
+ }
+ }()
+}
+
+func (s *Server) startPublic(ctx context.Context) {
+ g := grpc.NewServer(grpc.Creds(insecure.NewCredentials()))
+ lis, err := net.Listen("tcp", s.Config.PublicListenAddress)
+ if err != nil {
+ klog.Exitf("Could not listen: %v", err)
+ }
+ s.ListenPublic = lis.Addr().String()
+
+ reflection.Register(g)
+ apb.RegisterIAMServer(g, s)
+
+ wrapped := grpcweb.WrapServer(g)
+ server := http.Server{
+ Addr: s.Config.PublicListenAddress,
+ Handler: http.HandlerFunc(wrapped.ServeHTTP),
+ }
+ klog.Infof("Public API listening on %s", s.ListenPublic)
+ go func() {
+ err := server.Serve(lis)
+ if err != ctx.Err() {
+ klog.Exitf("Public API serve failed: %v", err)
+ }
+ }()
+}
+
+// Start runs the two listeners of the server. The process will fail (via
+// klog.Exit) if any of the listeners/servers fail to start.
+func (s *Server) Start(ctx context.Context) {
+ s.startInternalGRPC(ctx)
+ s.startPublic(ctx)
+}
+
+func (s *Server) WhoAmI(ctx context.Context, req *apb.WhoAmIRequest) (*apb.WhoAmIResponse, error) {
+ klog.Infof("req: %+v", req)
+ return nil, status.Error(codes.Unimplemented, "unimplemented")
+}
diff --git a/cloud/apigw/server/server_test.go b/cloud/apigw/server/server_test.go
new file mode 100644
index 0000000..16cdc07
--- /dev/null
+++ b/cloud/apigw/server/server_test.go
@@ -0,0 +1,74 @@
+package server
+
+import (
+ "bytes"
+ "context"
+ "encoding/binary"
+ "fmt"
+ "net/http"
+ "strconv"
+ "testing"
+
+ "google.golang.org/grpc/codes"
+ "google.golang.org/protobuf/proto"
+
+ apb "source.monogon.dev/cloud/api"
+ "source.monogon.dev/cloud/lib/component"
+)
+
+// TestPublicSimple ensures the public grpc-web listener is working.
+func TestPublicSimple(t *testing.T) {
+ s := Server{
+ Config: Config{
+ Configuration: component.Configuration{
+ GRPCListenAddress: ":0",
+ DevCerts: true,
+ DevCertsPath: "/tmp/foo",
+ },
+ PublicListenAddress: ":0",
+ },
+ }
+
+ ctx := context.Background()
+ s.Start(ctx)
+
+ // Craft a gRPC-Web request from scratch. There doesn't seem to be a
+ // well-supported library to do this.
+
+ // The request is \0 ++ uint32be(len(req)) ++ req.
+ msgBytes, err := proto.Marshal(&apb.WhoAmIRequest{})
+ if err != nil {
+ t.Fatalf("Could not marshal request body: %v", err)
+ }
+ buf := bytes.NewBuffer(nil)
+ binary.Write(buf, binary.BigEndian, byte(0))
+ binary.Write(buf, binary.BigEndian, uint32(len(msgBytes)))
+ buf.Write(msgBytes)
+
+ // Perform the request. Set minimum headers required for gRPC-Web to recognize
+ // this as a gRPC-Web request.
+ req, err := http.NewRequest("POST", fmt.Sprintf("http://%s/cloud.api.IAM/WhoAmI", s.ListenPublic), buf)
+ if err != nil {
+ t.Fatalf("Could not create request: %v", err)
+ }
+ req.Header.Set("Content-Type", "application/grpc-web+proto")
+ req.Header.Set("X-Grpc-Web", "1")
+
+ res, err := http.DefaultClient.Do(req)
+ if err != nil {
+ t.Fatalf("Could not perform request: %v", err)
+ }
+ // Regardless for RPC status, 200 should always be returned.
+ if want, got := 200, res.StatusCode; want != got {
+ t.Errorf("Wanted code %d, got %d", want, got)
+ }
+
+ // Expect endpoint to return 'unimplemented'.
+ code, _ := strconv.Atoi(res.Header.Get("Grpc-Status"))
+ if want, got := uint32(codes.Unimplemented), uint32(code); want != got {
+ t.Errorf("Wanted code %d, got %d", want, got)
+ }
+ if want, got := "unimplemented", res.Header.Get("Grpc-Message"); want != got {
+ t.Errorf("Wanted message %q, got %q", want, got)
+ }
+}
diff --git a/cloud/lib/component/BUILD.bazel b/cloud/lib/component/BUILD.bazel
new file mode 100644
index 0000000..a97d770
--- /dev/null
+++ b/cloud/lib/component/BUILD.bazel
@@ -0,0 +1,18 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+ name = "component",
+ srcs = [
+ "component.go",
+ "devcerts.go",
+ ],
+ importpath = "source.monogon.dev/cloud/lib/component",
+ visibility = ["//visibility:public"],
+ deps = [
+ "//metropolis/pkg/pki",
+ "@com_github_adrg_xdg//:xdg",
+ "@io_k8s_klog_v2//:klog",
+ "@org_golang_google_grpc//:go_default_library",
+ "@org_golang_google_grpc//credentials",
+ ],
+)
diff --git a/cloud/lib/component/component.go b/cloud/lib/component/component.go
new file mode 100644
index 0000000..4353bdf
--- /dev/null
+++ b/cloud/lib/component/component.go
@@ -0,0 +1,105 @@
+// Package component implements reusable bits for cloud service components. Each
+// component is currently defined as being a standalone Go binary with its own
+// internal gRPC listener. Subsequent listeners (eg. public gRPC or HTTP) can be
+// defined by users of this library.
+package component
+
+import (
+ "crypto/tls"
+ "crypto/x509"
+ "flag"
+ "os"
+ "path/filepath"
+
+ "github.com/adrg/xdg"
+ "google.golang.org/grpc"
+ "google.golang.org/grpc/credentials"
+ "k8s.io/klog/v2"
+)
+
+// Configuration is the common configuration of a component.
+type Configuration struct {
+ // GRPCKeyPath is the filesystem path of the x509 key used to serve internal
+ // gRPC traffic.
+ GRPCKeyPath string
+ // GRPCCertificatePath is the filesystem path of the x509 certificate used to
+ // serve internal gRPC traffic.
+ GRPCCertificatePath string
+ // GRPCCAPath is the filesystem path of of the x509 CA certificate used to
+ // verify incoming connections on internal gRPC traffic.
+ GRPCCAPath string
+ // GRPCListenAddress is the address on which the component should server
+ // internal gRPC traffic.
+ GRPCListenAddress string
+
+ // DevCerts, if enabled, automatically generates development CA and component
+ // certificates/keys at DevCertsPath, uses these to serve traffic.
+ DevCerts bool
+ // DevCertsPath sets the prefix in which DevCerts are generated. All components
+ // should have the same path set so that they reuse the CA certificate.
+ DevCertsPath string
+
+ // ComponentName is the name of this component, which should be [a-z0-9+]. It's
+ // used to prefix all flags set by the Configuration.
+ ComponentName string
+}
+
+// RegisterFlags registers the component configuration to be provided by flags.
+// This must be called exactly once before then calling flags.Parse().
+func (c *Configuration) RegisterFlags(componentName string) {
+ flag.StringVar(&c.GRPCKeyPath, componentName+"_grpc_key_path", "", "Path to gRPC server/client key for "+componentName)
+ flag.StringVar(&c.GRPCCertificatePath, componentName+"_grpc_certificate_path", "", "Path to gRPC server/client certificate for "+componentName)
+ flag.StringVar(&c.GRPCCAPath, componentName+"_grpc_ca_certificate_path", "", "Path to gRPC CA certificate for "+componentName)
+ flag.StringVar(&c.GRPCListenAddress, componentName+"_grpc_listen_address", ":4242", "Address to listen at for gRPC connections for "+componentName)
+
+ flag.BoolVar(&c.DevCerts, componentName+"_dev_certs", false, "Use developer certificates (autogenerated) for "+componentName)
+ flag.StringVar(&c.DevCertsPath, componentName+"_dev_certs_path", filepath.Join(xdg.ConfigHome, "monogon-dev-certs"), "Path for storing developer certificates")
+
+ c.ComponentName = componentName
+}
+
+// GRPCServerOptions returns pre-built grpc.ServerOptions that this component
+// should use to serve internal gRPC.
+func (c *Configuration) GRPCServerOptions() []grpc.ServerOption {
+ var certPath, keyPath, caPath string
+ if c.DevCerts {
+ // Use devcerts if requested.
+ certPath, keyPath, caPath = c.GetDevCerts()
+ } else {
+ // Otherwise, use data from flags.
+ if c.GRPCKeyPath == "" {
+ klog.Exitf("-grpc_key_path must be set")
+ }
+ if c.GRPCCertificatePath == "" {
+ klog.Exitf("-grpc_certificate_path must be set")
+ }
+ if c.GRPCCAPath == "" {
+ klog.Exitf("-grpc_ca_certificate_path must be set")
+ }
+ keyPath = c.GRPCKeyPath
+ certPath = c.GRPCCertificatePath
+ caPath = c.GRPCCAPath
+ }
+
+ ca, err := os.ReadFile(caPath)
+ if err != nil {
+ klog.Exitf("Could not read GRPC CA: %v", err)
+ }
+ certPool := x509.NewCertPool()
+ if !certPool.AppendCertsFromPEM(ca) {
+ klog.Exitf("Could not load GRPC CA: %v", err)
+ }
+
+ pair, err := tls.LoadX509KeyPair(certPath, keyPath)
+ if err != nil {
+ klog.Exitf("Could not load GRPC TLS keypair: %v", err)
+ }
+ tlsConf := &tls.Config{
+ Certificates: []tls.Certificate{pair},
+ ClientAuth: tls.RequireAndVerifyClientCert,
+ RootCAs: certPool,
+ }
+ return []grpc.ServerOption{
+ grpc.Creds(credentials.NewTLS(tlsConf)),
+ }
+}
diff --git a/cloud/lib/component/devcerts.go b/cloud/lib/component/devcerts.go
new file mode 100644
index 0000000..af1e3d3
--- /dev/null
+++ b/cloud/lib/component/devcerts.go
@@ -0,0 +1,187 @@
+package component
+
+import (
+ "crypto/ed25519"
+ "crypto/rand"
+ "crypto/tls"
+ "crypto/x509"
+ "encoding/pem"
+ "fmt"
+ "math/big"
+ "os"
+ "time"
+
+ "k8s.io/klog/v2"
+
+ "source.monogon.dev/metropolis/pkg/pki"
+)
+
+// GetDevCerts returns paths to this component's development certificate, key
+// and CA, or panics if unavailable.
+func (c *Configuration) GetDevCerts() (certPath, keyPath, caPath string) {
+ klog.Infof("Using developer certificates at %s", c.DevCertsPath)
+
+ caPath = c.ensureDevCA()
+ certPath, keyPath = c.ensureDevComponent()
+ return
+}
+
+// ensureDevComponent ensures that a development certificate/key exists for this
+// component and returns paths to them. This data is either read from disk if it
+// already exists, or is generated when this function is called. If any problem
+// occurs, the code panics.
+func (c *Configuration) ensureDevComponent() (certPath, keyPath string) {
+ caKeyPath := c.DevCertsPath + "/ca.key"
+ caCertPath := c.DevCertsPath + "/ca.cert"
+
+ // Load CA. By convention, we are always called after ensureDevCA.
+ ca, err := tls.LoadX509KeyPair(caCertPath, caKeyPath)
+ if err != nil {
+ klog.Exitf("Could not load Dev CA: %v", err)
+ }
+ caCert, err := x509.ParseCertificate(ca.Certificate[0])
+ if err != nil {
+ klog.Exitf("Could not parse Dev CA: %v", err)
+ }
+
+ // Check if we have keys already.
+ keyPath = c.DevCertsPath + fmt.Sprintf("/%s.key", c.ComponentName)
+ certPath = c.DevCertsPath + fmt.Sprintf("/%s.crt", c.ComponentName)
+ noKey := false
+ if _, err := os.Stat(keyPath); os.IsNotExist(err) {
+ noKey = true
+ }
+ noCert := false
+ if _, err := os.Stat(certPath); os.IsNotExist(err) {
+ noCert = true
+ }
+
+ if noKey || noCert {
+ klog.Infof("Generating developer %s certificate...", c.ComponentName)
+ } else {
+ return
+ }
+
+ // Generate key/certificate.
+ cert := pki.Server([]string{
+ fmt.Sprintf("%s.local", c.ComponentName),
+ }, nil)
+
+ serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 127)
+ serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
+ if err != nil {
+ klog.Exitf("Failed to generate %s serial number: %v", c.ComponentName, err)
+ }
+ cert.SerialNumber = serialNumber
+ cert.NotBefore = time.Now()
+ cert.NotAfter = pki.UnknownNotAfter
+ cert.BasicConstraintsValid = true
+
+ pub, priv, err := ed25519.GenerateKey(rand.Reader)
+ if err != nil {
+ klog.Exitf("Failed to generate %s key: %v", c.ComponentName, err)
+ }
+ certBytes, err := x509.CreateCertificate(rand.Reader, &cert, caCert, pub, ca.PrivateKey)
+ if err != nil {
+ klog.Exitf("Failed to generate %s certificate: %v", c.ComponentName, err)
+ }
+
+ // And marshal them to disk.
+ privPKCS, err := x509.MarshalPKCS8PrivateKey(priv)
+ if err != nil {
+ klog.Exitf("Failed to marshal %s private key: %v", c.ComponentName, err)
+ }
+ err = os.WriteFile(keyPath, pem.EncodeToMemory(&pem.Block{
+ Type: "PRIVATE KEY",
+ Bytes: privPKCS,
+ }), 0600)
+ if err != nil {
+ klog.Exitf("Failed to write %s private key: %v", c.ComponentName, err)
+ }
+ err = os.WriteFile(certPath, pem.EncodeToMemory(&pem.Block{
+ Type: "CERTIFICATE",
+ Bytes: certBytes,
+ }), 0644)
+ if err != nil {
+ klog.Exitf("Failed to write %s certificate: %v", c.ComponentName, err)
+ }
+
+ return
+}
+
+// ensureDevCA ensures that a development CA certificate/key exists and returns
+// paths to them. This data is either read from disk if it already exists, or is
+// generated when this function is called. If any problem occurs, the code
+// panics.
+func (c *Configuration) ensureDevCA() (caCertPath string) {
+ caKeyPath := c.DevCertsPath + "/ca.key"
+ caCertPath = c.DevCertsPath + "/ca.cert"
+
+ if err := os.MkdirAll(c.DevCertsPath, 0700); err != nil {
+ klog.Exitf("Failed to make developer certificate directory: %v", err)
+ }
+
+ // Check if we already have a key/certificate.
+ noKey := false
+ if _, err := os.Stat(caKeyPath); os.IsNotExist(err) {
+ noKey = true
+ }
+ noCert := false
+ if _, err := os.Stat(caCertPath); os.IsNotExist(err) {
+ noCert = true
+ }
+
+ if noKey || noCert {
+ klog.Infof("Generating developer CA certificate...")
+ } else {
+ return
+ }
+ hostname, err := os.Hostname()
+ if err != nil {
+ hostname = "unknown"
+ }
+
+ // No key/certificate, generate them.
+ ca := pki.CA(fmt.Sprintf("monogon dev certs CA (%s)", hostname))
+
+ serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 127)
+ serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
+ if err != nil {
+ klog.Exitf("Failed to generate CA serial number: %v", err)
+ }
+ ca.SerialNumber = serialNumber
+ ca.NotBefore = time.Now()
+ ca.NotAfter = pki.UnknownNotAfter
+ ca.BasicConstraintsValid = true
+
+ caPub, caPriv, err := ed25519.GenerateKey(rand.Reader)
+ if err != nil {
+ klog.Exitf("Failed to generate CA key: %v", err)
+ }
+ caBytes, err := x509.CreateCertificate(rand.Reader, &ca, &ca, caPub, caPriv)
+ if err != nil {
+ klog.Exitf("Failed to generate CA certificate: %v", err)
+ }
+
+ // And marshal them to disk.
+ caPrivPKCS, err := x509.MarshalPKCS8PrivateKey(caPriv)
+ if err != nil {
+ klog.Exitf("Failed to marshal %s private key: %v", c.ComponentName, err)
+ }
+ err = os.WriteFile(caKeyPath, pem.EncodeToMemory(&pem.Block{
+ Type: "PRIVATE KEY",
+ Bytes: caPrivPKCS,
+ }), 0600)
+ if err != nil {
+ klog.Exitf("Failed to write CA private key: %v", err)
+ }
+ err = os.WriteFile(caCertPath, pem.EncodeToMemory(&pem.Block{
+ Type: "CERTIFICATE",
+ Bytes: caBytes,
+ }), 0644)
+ if err != nil {
+ klog.Exitf("Failed to write CA certificate: %v", err)
+ }
+
+ return
+}