Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Read secrets for client-onboarding-token-validation #2827

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions controllers/storagecluster/storageclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@ import (
)

const (
tokenLifetimeInHours = 48
onboardingPrivateKeyFilePath = "/etc/private-key/key"
tokenLifetimeInHours = 48

ocsClientConfigMapName = "ocs-client-operator-config"
deployCSIKey = "DEPLOY_CSI"
Expand All @@ -42,9 +41,14 @@ func (s *storageClient) ensureCreated(r *StorageClusterReconciler, storagecluste

storageClient := &ocsclientv1a1.StorageClient{}
storageClient.Name = storagecluster.Name
storageClusterNamespace := storagecluster.Namespace
_, err := controllerutil.CreateOrUpdate(r.ctx, r.Client, storageClient, func() error {
if storageClient.Status.ConsumerID == "" {
token, err := util.GenerateClientOnboardingToken(tokenLifetimeInHours, onboardingPrivateKeyFilePath, nil)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's remove onboardingPrivateKeyFilePath if it is not used anymore

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ack

privateKey, err := util.GetParsedPrivateKey(r.ctx, r.Client, storageClusterNamespace)
if err != nil {
return fmt.Errorf("unable to get Parsed Private Key: %v", err)
}
token, err := util.GenerateClientOnboardingToken(tokenLifetimeInHours, privateKey, nil)
if err != nil {
return fmt.Errorf("unable to generate onboarding token: %v", err)
}
Expand Down
2 changes: 1 addition & 1 deletion controllers/storagecluster/storagecluster_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ func (r *StorageClusterReconciler) SetupWithManager(mgr ctrl.Manager) error {
Owns(&appsv1.Deployment{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})).
Owns(&corev1.Service{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})).
Owns(&corev1.ConfigMap{}, builder.MatchEveryOwner, builder.WithPredicates(predicate.GenerationChangedPredicate{})).
Owns(&corev1.Secret{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})).
Owns(&corev1.Secret{}, builder.MatchEveryOwner, builder.WithPredicates(predicate.GenerationChangedPredicate{})).
Owns(&routev1.Route{}).
Owns(&templatev1.Template{}).
Watches(&storagev1.StorageClass{}, enqueueStorageClusterRequest).
Expand Down
35 changes: 20 additions & 15 deletions controllers/util/provider.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package util

import (
"context"
"crypto"
"crypto/rand"
"crypto/rsa"
Expand All @@ -10,17 +11,20 @@ import (
"encoding/json"
"encoding/pem"
"fmt"
"os"
"time"

"github.com/google/uuid"
"github.com/red-hat-storage/ocs-operator/v4/services"
corev1 "k8s.io/api/core/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
)

const onboardingValidationPrivateKeySecretName = "onboarding-private-key"

Comment on lines +22 to +23
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is also defined in onboarding-ticket-generator, could we make sure that we are not defining at only one place

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

onboardingValidationPrivateKeySecretName is used at two places >

  1. provider.go
  2. onboarding-validation-keys-generator

// GenerateClientOnboardingToken generates a ocs-client token valid for a duration of "tokenLifetimeInHours".
// The token content is predefined and signed by the private key which'll be read from supplied "privateKeyPath".
// The storageQuotaInGiB is optional, and it is used to limit the storage of PVC in the application cluster.
func GenerateClientOnboardingToken(tokenLifetimeInHours int, privateKeyPath string, storageQuotainGib *uint) (string, error) {
func GenerateClientOnboardingToken(tokenLifetimeInHours int, privateKey *rsa.PrivateKey, storageQuotainGib *uint) (string, error) {
tokenExpirationDate := time.Now().
Add(time.Duration(tokenLifetimeInHours) * time.Hour).
Unix()
Expand All @@ -32,7 +36,7 @@ func GenerateClientOnboardingToken(tokenLifetimeInHours int, privateKeyPath stri
StorageQuotaInGiB: storageQuotainGib,
}

token, err := encodeAndSignOnboardingToken(privateKeyPath, ticket)
token, err := encodeAndSignOnboardingToken(privateKey, ticket)
if err != nil {
return "", err
}
Expand All @@ -41,7 +45,7 @@ func GenerateClientOnboardingToken(tokenLifetimeInHours int, privateKeyPath stri

// GeneratePeerOnboardingToken generates a ocs-peer token valid for a duration of "tokenLifetimeInHours".
// The token content is predefined and signed by the private key which'll be read from supplied "privateKeyPath".
func GeneratePeerOnboardingToken(tokenLifetimeInHours int, privateKeyPath string) (string, error) {
func GeneratePeerOnboardingToken(tokenLifetimeInHours int, privateKey *rsa.PrivateKey) (string, error) {
tokenExpirationDate := time.Now().
Add(time.Duration(tokenLifetimeInHours) * time.Hour).
Unix()
Expand All @@ -51,7 +55,7 @@ func GeneratePeerOnboardingToken(tokenLifetimeInHours int, privateKeyPath string
ExpirationDate: tokenExpirationDate,
SubjectRole: services.PeerRole,
}
token, err := encodeAndSignOnboardingToken(privateKeyPath, ticket)
token, err := encodeAndSignOnboardingToken(privateKey, ticket)
if err != nil {
return "", err
}
Expand All @@ -60,7 +64,7 @@ func GeneratePeerOnboardingToken(tokenLifetimeInHours int, privateKeyPath string

// encodeAndSignOnboardingToken generates a token from the ticket.
// The token content is predefined and signed by the private key which'll be read from supplied "privateKeyPath".
func encodeAndSignOnboardingToken(privateKeyPath string, ticket services.OnboardingTicket) (string, error) {
func encodeAndSignOnboardingToken(privateKey *rsa.PrivateKey, ticket services.OnboardingTicket) (string, error) {
payload, err := json.Marshal(ticket)
if err != nil {
return "", fmt.Errorf("failed to marshal the payload: %v", err)
Expand All @@ -75,11 +79,6 @@ func encodeAndSignOnboardingToken(privateKeyPath string, ticket services.Onboard
return "", fmt.Errorf("failed to hash onboarding token payload: %v", err)
}

privateKey, err := readAndDecodePrivateKey(privateKeyPath)
if err != nil {
return "", fmt.Errorf("failed to read and decode private key: %v", err)
}

msgHashSum := msgHash.Sum(nil)
// In order to generate the signature, we provide a random number generator,
// our private key, the hashing algorithm that we used, and the hash sum
Expand All @@ -93,16 +92,22 @@ func encodeAndSignOnboardingToken(privateKeyPath string, ticket services.Onboard
return fmt.Sprintf("%s.%s", encodedPayload, encodedSignature), nil
}

func readAndDecodePrivateKey(privateKeyPath string) (*rsa.PrivateKey, error) {
pemString, err := os.ReadFile(privateKeyPath)
func GetParsedPrivateKey(ctx context.Context, cl client.Client, ns string) (*rsa.PrivateKey, error) {
privateSecret := &corev1.Secret{}
privateSecret.Name = onboardingValidationPrivateKeySecretName
privateSecret.Namespace = ns

err := cl.Get(ctx, client.ObjectKeyFromObject(privateSecret), privateSecret)
if err != nil {
return nil, fmt.Errorf("failed to read private key: %v", err)
return nil, fmt.Errorf("failed to get private secret: %v", err)
}

Block, _ := pem.Decode(pemString)
Block, _ := pem.Decode(privateSecret.Data["key"])
privateKey, err := x509.ParsePKCS1PrivateKey(Block.Bytes)

if err != nil {
return nil, fmt.Errorf("failed to parse private key: %v", err)
}

return privateKey, nil
}
Original file line number Diff line number Diff line change
Expand Up @@ -629,6 +629,10 @@ spec:
- name: ONBOARDING_TOKEN_LIFETIME
- name: UX_BACKEND_PORT
- name: TLS_ENABLED
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
image: quay.io/ocs-dev/ocs-operator:latest
imagePullPolicy: IfNotPresent
name: ux-backend-server
Expand All @@ -639,8 +643,6 @@ spec:
readOnlyRootFilesystem: true
runAsNonRoot: true
volumeMounts:
- mountPath: /etc/private-key
name: onboarding-private-key
- mountPath: /etc/tls/private
name: ux-cert-secret
- args:
Expand Down Expand Up @@ -676,10 +678,6 @@ spec:
operator: Equal
value: "true"
volumes:
- name: onboarding-private-key
secret:
optional: true
secretName: onboarding-private-key
- name: ux-proxy-secret
secret:
secretName: ux-backend-proxy
Expand Down
23 changes: 15 additions & 8 deletions services/ux-backend/handlers/onboarding/clienttokens/handler.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package clienttokens

import (
"context"
"encoding/json"
"fmt"
"math"
Expand All @@ -11,10 +12,7 @@ import (

"k8s.io/klog/v2"
"k8s.io/utils/ptr"
)

const (
onboardingPrivateKeyFilePath = "/etc/private-key/key"
"sigs.k8s.io/controller-runtime/pkg/client"
)

var unitToGib = map[string]uint{
Expand All @@ -23,20 +21,29 @@ var unitToGib = map[string]uint{
"Pi": 1024 * 1024,
}

func HandleMessage(w http.ResponseWriter, r *http.Request, tokenLifetimeInHours int) {
func HandleMessage(w http.ResponseWriter, r *http.Request, tokenLifetimeInHours int, cl client.Client, ns string) {
switch r.Method {
case "POST":
handlePost(w, r, tokenLifetimeInHours)
handlePost(w, r, tokenLifetimeInHours, cl, ns)
default:
handleUnsupportedMethod(w, r)
}
}

func handlePost(w http.ResponseWriter, r *http.Request, tokenLifetimeInHours int) {
func handlePost(w http.ResponseWriter, r *http.Request, tokenLifetimeInHours int, cl client.Client, ns string) {
var storageQuotaInGiB *uint
// When ContentLength is 0 that means request body is empty and
// storage quota is unlimited
var err error

ctx := context.TODO()
privateKey, err := util.GetParsedPrivateKey(ctx, cl, ns)
klog.Info("Getting the Pem key")
if err != nil {
http.Error(w, fmt.Sprintf("Failed to get private key: %v", err), http.StatusBadRequest)
return
}

if r.ContentLength != 0 {
var quota = struct {
Value uint `json:"value"`
Expand All @@ -59,7 +66,7 @@ func handlePost(w http.ResponseWriter, r *http.Request, tokenLifetimeInHours int
storageQuotaInGiB = ptr.To(unitAsGiB * quota.Value)
}

if onboardingToken, err := util.GenerateClientOnboardingToken(tokenLifetimeInHours, onboardingPrivateKeyFilePath, storageQuotaInGiB); err != nil {
if onboardingToken, err := util.GenerateClientOnboardingToken(tokenLifetimeInHours, privateKey, storageQuotaInGiB); err != nil {
klog.Errorf("failed to get onboarding token: %v", err)
w.WriteHeader(http.StatusInternalServerError)
w.Header().Set("Content-Type", handlers.ContentTypeTextPlain)
Expand Down
24 changes: 16 additions & 8 deletions services/ux-backend/handlers/onboarding/peertokens/handler.go
Original file line number Diff line number Diff line change
@@ -1,30 +1,38 @@
package peertokens

import (
"context"
"fmt"
"net/http"

"github.com/red-hat-storage/ocs-operator/v4/controllers/util"
"github.com/red-hat-storage/ocs-operator/v4/services/ux-backend/handlers"

"k8s.io/klog/v2"
"sigs.k8s.io/controller-runtime/pkg/client"
)

const (
onboardingPrivateKeyFilePath = "/etc/private-key/key"
)

func HandleMessage(w http.ResponseWriter, r *http.Request, tokenLifetimeInHours int) {
func HandleMessage(w http.ResponseWriter, r *http.Request, tokenLifetimeInHours int, cl client.Client, ns string) {
switch r.Method {
case "POST":
handlePost(w, r, tokenLifetimeInHours)
handlePost(w, r, tokenLifetimeInHours, cl, ns)
default:
handleUnsupportedMethod(w, r)
}
}

func handlePost(w http.ResponseWriter, _ *http.Request, tokenLifetimeInHours int) {
if onboardingToken, err := util.GeneratePeerOnboardingToken(tokenLifetimeInHours, onboardingPrivateKeyFilePath); err != nil {
func handlePost(w http.ResponseWriter, _ *http.Request, tokenLifetimeInHours int, cl client.Client, ns string) {
var err error

ctx := context.TODO()
privateKey, err := util.GetParsedPrivateKey(ctx, cl, ns)
klog.Info("Getting the Pem key")
if err != nil {
http.Error(w, fmt.Sprintf("Failed to get private key: %v", err), http.StatusBadRequest)
return
}

if onboardingToken, err := util.GeneratePeerOnboardingToken(tokenLifetimeInHours, privateKey); err != nil {
klog.Errorf("failed to get onboarding token: %v", err)
w.WriteHeader(http.StatusInternalServerError)
w.Header().Set("Content-Type", handlers.ContentTypeTextPlain)
Expand Down
37 changes: 34 additions & 3 deletions services/ux-backend/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,15 @@ import (
"os"
"strconv"

"github.com/red-hat-storage/ocs-operator/v4/controllers/util"
"github.com/red-hat-storage/ocs-operator/v4/services/ux-backend/handlers/onboarding/clienttokens"
"github.com/red-hat-storage/ocs-operator/v4/services/ux-backend/handlers/onboarding/peertokens"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"

"k8s.io/klog/v2"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/config"
)

type serverConfig struct {
Expand Down Expand Up @@ -63,18 +68,25 @@ func main() {
os.Exit(-1)
}

cl, err := newClient()
if err != nil {
klog.Exitf("failed to create client: %v", err)
}

ns := util.GetPodNamespace()

// TODO: remove '/onboarding-tokens' in the future
http.HandleFunc("/onboarding-tokens", func(w http.ResponseWriter, r *http.Request) {
// Set the Deprecation header
w.Header().Set("Deprecation", "true") // Standard "Deprecation" header
w.Header().Set("Link", "/onboarding/client-tokens; rel=\"alternate\"")
clienttokens.HandleMessage(w, r, config.tokenLifetimeInHours)
clienttokens.HandleMessage(w, r, config.tokenLifetimeInHours, cl, ns)
})
http.HandleFunc("/onboarding/client-tokens", func(w http.ResponseWriter, r *http.Request) {
clienttokens.HandleMessage(w, r, config.tokenLifetimeInHours)
clienttokens.HandleMessage(w, r, config.tokenLifetimeInHours, cl, ns)
})
http.HandleFunc("/onboarding/peer-tokens", func(w http.ResponseWriter, r *http.Request) {
peertokens.HandleMessage(w, r, config.tokenLifetimeInHours)
peertokens.HandleMessage(w, r, config.tokenLifetimeInHours, cl, ns)
})

klog.Info("ux backend server listening on port ", config.listenPort)
Expand All @@ -94,3 +106,22 @@ func main() {
log.Fatal(err)

}

func newClient() (client.Client, error) {
klog.Info("Setting up k8s client")
scheme := runtime.NewScheme()
if err := corev1.AddToScheme(scheme); err != nil {
return nil, err
}

config, err := config.GetConfig()
if err != nil {
return nil, err
}
k8sClient, err := client.New(config, client.Options{Scheme: scheme})
if err != nil {
return nil, err
}

return k8sClient, nil
}
21 changes: 8 additions & 13 deletions tools/csv-merger/csv-merger.go
Original file line number Diff line number Diff line change
Expand Up @@ -644,10 +644,6 @@ func getUXBackendServerDeployment() appsv1.DeploymentSpec {
{
Name: "ux-backend-server",
VolumeMounts: []corev1.VolumeMount{
{
Name: "onboarding-private-key",
MountPath: "/etc/private-key",
},
{
Name: "ux-cert-secret",
MountPath: "/etc/tls/private",
Expand All @@ -674,6 +670,14 @@ func getUXBackendServerDeployment() appsv1.DeploymentSpec {
Name: "TLS_ENABLED",
Value: os.Getenv("TLS_ENABLED"),
},
{
Name: util.PodNamespaceEnvVar,
ValueFrom: &corev1.EnvVarSource{
FieldRef: &corev1.ObjectFieldSelector{
FieldPath: "metadata.namespace",
},
},
},
},
SecurityContext: &corev1.SecurityContext{
RunAsNonRoot: ptr.To(true),
Expand Down Expand Up @@ -716,15 +720,6 @@ func getUXBackendServerDeployment() appsv1.DeploymentSpec {
},
},
Volumes: []corev1.Volume{
{
Name: "onboarding-private-key",
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{
SecretName: "onboarding-private-key",
Optional: ptr.To(true),
},
},
},
{
Name: "ux-proxy-secret",
VolumeSource: corev1.VolumeSource{
Expand Down
Loading