From 529643bd3f821034ec96eba373dfdcc9c422a5bb Mon Sep 17 00:00:00 2001 From: Leela Venkaiah G Date: Tue, 20 Feb 2024 17:53:46 +0530 Subject: [PATCH] ux: implement rotating keys api Implement rotating keys by deleting the secret containing onboarding public key. The expectation is this new api endpoint be called via CLI or UI and get the existing private & public key pairs rotated. Signed-off-by: Leela Venkaiah G --- rbac/ux_backend_role.yaml | 8 +++ services/ux-backend/handlers/common.go | 23 +++++++ .../handlers/onboardingtokens/handler.go | 8 +-- .../ux-backend/handlers/rotatekeys/handler.go | 62 +++++++++++++++++++ services/ux-backend/main.go | 27 ++++++++ tools/csv-merger/csv-merger.go | 8 +++ 6 files changed, 132 insertions(+), 4 deletions(-) create mode 100644 services/ux-backend/handlers/common.go create mode 100644 services/ux-backend/handlers/rotatekeys/handler.go diff --git a/rbac/ux_backend_role.yaml b/rbac/ux_backend_role.yaml index f89b32672e..5f1bbd7f9d 100644 --- a/rbac/ux_backend_role.yaml +++ b/rbac/ux_backend_role.yaml @@ -14,3 +14,11 @@ rules: verbs: - get - list +- apiGroups: + - "" + resources: + - secrets + resourceNames: + - onboarding-ticket-key + verbs: + - delete diff --git a/services/ux-backend/handlers/common.go b/services/ux-backend/handlers/common.go new file mode 100644 index 0000000000..fb34e83171 --- /dev/null +++ b/services/ux-backend/handlers/common.go @@ -0,0 +1,23 @@ +package handlers + +import ( + "os" +) + +const ( + ContentTypeTextPlain = "text/plain" +) + +var namespace string + +// returns namespace found in env value, will panic if value is empty +func GetPodNamespace() string { + if namespace != "" { + return namespace + } + if ns := os.Getenv("POD_NAMESPACE"); ns != "" { + namespace = ns + return namespace + } + panic("Value for env var 'POD_NAMESPACE' is empty") +} diff --git a/services/ux-backend/handlers/onboardingtokens/handler.go b/services/ux-backend/handlers/onboardingtokens/handler.go index bef1c5e044..8560b8428c 100644 --- a/services/ux-backend/handlers/onboardingtokens/handler.go +++ b/services/ux-backend/handlers/onboardingtokens/handler.go @@ -16,12 +16,12 @@ import ( "github.com/google/uuid" "github.com/red-hat-storage/ocs-operator/v4/services" + "github.com/red-hat-storage/ocs-operator/v4/services/ux-backend/handlers" "k8s.io/klog/v2" ) const ( onboardingPrivateKeyFilePath = "/etc/private-key/key" - ContentTypeTextPlain = "text/plain" ) func HandleMessage(w http.ResponseWriter, r *http.Request, tokenLifetimeInHours int) { @@ -37,7 +37,7 @@ func handlePost(w http.ResponseWriter, tokenLifetimeInHours int) { if onboardingToken, err := generateOnboardingToken(tokenLifetimeInHours); err != nil { klog.Errorf("failed to get onboardig token: %v", err) w.WriteHeader(http.StatusInternalServerError) - w.Header().Set("Content-Type", ContentTypeTextPlain) + w.Header().Set("Content-Type", handlers.ContentTypeTextPlain) if _, err := w.Write([]byte("Failed to generate token")); err != nil { klog.Errorf("failed write data to response writer, %v", err) @@ -45,7 +45,7 @@ func handlePost(w http.ResponseWriter, tokenLifetimeInHours int) { } else { klog.Info("onboarding token generated successfully") w.WriteHeader(http.StatusOK) - w.Header().Set("Content-Type", ContentTypeTextPlain) + w.Header().Set("Content-Type", handlers.ContentTypeTextPlain) if _, err = w.Write([]byte(onboardingToken)); err != nil { klog.Errorf("failed write data to response writer: %v", err) @@ -56,7 +56,7 @@ func handlePost(w http.ResponseWriter, tokenLifetimeInHours int) { func handleUnsupportedMethod(w http.ResponseWriter, r *http.Request) { klog.Info("Only POST method should be used to send data to this endpoint /onboarding-tokens") w.WriteHeader(http.StatusMethodNotAllowed) - w.Header().Set("Content-Type", ContentTypeTextPlain) + w.Header().Set("Content-Type", handlers.ContentTypeTextPlain) w.Header().Set("Allow", "POST") if _, err := w.Write([]byte(fmt.Sprintf("Unsupported method : %s", r.Method))); err != nil { diff --git a/services/ux-backend/handlers/rotatekeys/handler.go b/services/ux-backend/handlers/rotatekeys/handler.go new file mode 100644 index 0000000000..fe87ebd9c6 --- /dev/null +++ b/services/ux-backend/handlers/rotatekeys/handler.go @@ -0,0 +1,62 @@ +package rotatekeys + +import ( + "context" + "fmt" + "net/http" + + "github.com/red-hat-storage/ocs-operator/v4/services/ux-backend/handlers" + corev1 "k8s.io/api/core/v1" + "k8s.io/klog/v2" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + onboardingValidationPublicKeySecretName = "onboarding-ticket-key" +) + +func HandleMessage(w http.ResponseWriter, r *http.Request, cl client.Client) { + switch r.Method { + case "POST": + handlePost(r.Context(), w, cl) + default: + handleUnsupportedMethod(w, r) + } +} + +func handlePost(ctx context.Context, w http.ResponseWriter, cl client.Client) { + klog.Info("POST method on /rotate-keys endpoint is invoked") + w.Header().Set("Content-Type", handlers.ContentTypeTextPlain) + + publicKeySecret := &corev1.Secret{} + publicKeySecret.Name = onboardingValidationPublicKeySecretName + publicKeySecret.Namespace = handlers.GetOperatorNamespace() + err := cl.Delete(ctx, publicKeySecret) + if err != nil { + klog.Errorf("failed to delete public key secret: %v", err) + w.WriteHeader(http.StatusInternalServerError) + + // TODO: should we differentiate b/n secret not found and remaining errors? + if _, err = w.Write([]byte("Failed to rotate keys")); err != nil { + klog.Errorf("failed to write data to response writer, %v", err) + } + return + } + + klog.Info("onboarding validation keys are rotated successfully") + w.WriteHeader(http.StatusOK) + if _, err = w.Write([]byte("Successfully rotated keys")); err != nil { + klog.Errorf("failed to write data to response writer, %v", err) + } +} + +func handleUnsupportedMethod(w http.ResponseWriter, r *http.Request) { + klog.Info("Only POST method should be used to send data to this endpoint /rotate-keys") + w.WriteHeader(http.StatusMethodNotAllowed) + w.Header().Set("Content-Type", handlers.ContentTypeTextPlain) + w.Header().Set("Allow", "POST") + + if _, err := w.Write([]byte(fmt.Sprintf("Unsupported method : %s", r.Method))); err != nil { + klog.Errorf("failed to write data to response writer: %v", err) + } +} diff --git a/services/ux-backend/main.go b/services/ux-backend/main.go index 5651379225..2ff778d812 100644 --- a/services/ux-backend/main.go +++ b/services/ux-backend/main.go @@ -10,6 +10,9 @@ import ( "k8s.io/klog/v2" "github.com/red-hat-storage/ocs-operator/v4/services/ux-backend/handlers/onboardingtokens" + "github.com/red-hat-storage/ocs-operator/v4/services/ux-backend/handlers/rotatekeys" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/config" ) type serverConfig struct { @@ -51,6 +54,20 @@ func loadAndValidateServerConfig() (*serverConfig, error) { return &config, nil } +func newKubeClient() (client.Client, error) { + cfg, err := config.GetConfig() + if err != nil { + return nil, err + } + + newClient, err := client.New(cfg, client.Options{}) + if err != nil { + return nil, err + } + + return newClient, nil +} + func main() { klog.Info("Starting ux backend server") @@ -61,9 +78,19 @@ func main() { klog.Info("shutting down!") os.Exit(-1) } + + cl, err := newKubeClient() + if err != nil { + klog.Errorf("failed to create kubernetes api client: %v", err) + klog.Exit("shutting down!") + } + http.HandleFunc("/onboarding-tokens", func(w http.ResponseWriter, r *http.Request) { onboardingtokens.HandleMessage(w, r, config.tokenLifetimeInHours) }) + http.HandleFunc("/rotate-keys", func(w http.ResponseWriter, r *http.Request) { + rotatekeys.HandleMessage(w, r, cl) + }) klog.Info("ux backend server listening on port ", config.listenPort) diff --git a/tools/csv-merger/csv-merger.go b/tools/csv-merger/csv-merger.go index 36d8fc7da5..072cfa63cd 100644 --- a/tools/csv-merger/csv-merger.go +++ b/tools/csv-merger/csv-merger.go @@ -980,6 +980,14 @@ func getUXBackendServerDeployment() appsv1.DeploymentSpec { Name: "TLS_ENABLED", Value: os.Getenv("TLS_ENABLED"), }, + { + Name: "POD_NAMESPACE", + ValueFrom: &corev1.EnvVarSource{ + FieldRef: &corev1.ObjectFieldSelector{ + FieldPath: "metadata.namespace", + }, + }, + }, }, }, {