Skip to content
Merged
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
8 changes: 5 additions & 3 deletions cmd/liqo-controller-manager/modules/authentication.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ type AuthOption struct {
APIServerAddressOverride string
CAOverrideB64 string
TrustedCA bool
TLSCompatibilityMode bool
SliceStatusOptions *remoteresourceslicecontroller.SliceStatusOptions
}

Expand All @@ -61,6 +62,7 @@ func NewAuthOption(identityProvider identitymanager.IdentityProvider, namespaceM
APIServerAddressOverride: opts.APIServerAddressOverride,
CAOverrideB64: opts.CAOverride,
TrustedCA: opts.TrustedCA,
TLSCompatibilityMode: opts.TLSCompatibilityMode,
SliceStatusOptions: &remoteresourceslicecontroller.SliceStatusOptions{
EnableStorage: opts.EnableStorage,
LocalRealStorageClassName: opts.RealStorageClassName,
Expand All @@ -85,7 +87,7 @@ func SetupAuthenticationModule(ctx context.Context, mgr manager.Manager, uncache
}
}

if err := enforceAuthenticationKeys(ctx, uncachedClient, opts.LiqoNamespace); err != nil {
if err := enforceAuthenticationKeys(ctx, uncachedClient, opts.LiqoNamespace, opts.TLSCompatibilityMode); err != nil {
klog.Errorf("Unable to enforce authentication keys: %v", err)
return err
}
Expand Down Expand Up @@ -178,8 +180,8 @@ func SetupAuthenticationModule(ctx context.Context, mgr manager.Manager, uncache
return nil
}

func enforceAuthenticationKeys(ctx context.Context, cl client.Client, liqoNamespace string) error {
if err := authentication.InitClusterKeys(ctx, cl, liqoNamespace); err != nil {
func enforceAuthenticationKeys(ctx context.Context, cl client.Client, liqoNamespace string, tlsCompatibilityMode bool) error {
if err := authentication.InitClusterKeys(ctx, cl, liqoNamespace, tlsCompatibilityMode); err != nil {
return err
}

Expand Down
1 change: 1 addition & 0 deletions deployments/liqo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
| authentication.awsConfig.secretAccessKey | string | `""` | SecretAccessKey for the Liqo user. |
| authentication.awsConfig.useExistingSecret | bool | `false` | Use an existing secret to configure the AWS credentials. |
| authentication.enabled | bool | `true` | Enable/Disable the authentication module. |
| authentication.tlsCompatibilityMode | bool | `false` | Enable TLS compatibility mode for client certificates and keys. If set to true, Liqo will use widely supported algorithm (RSA) instead of Ed25519 (default) for generating private keys and CSRs. Enable this option to ensure compatibility with systems that do not yet support Ed25519 as signature algorithm. |
| common.affinity | object | `{}` | Affinity for all liqo pods, excluding virtual kubelet. |
| common.extraArgs | list | `[]` | Extra arguments for all liqo pods, excluding virtual kubelet. |
| common.globalAnnotations | object | `{}` | Global annotations to be added to all resources created by Liqo controllers |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ spec:
- --liqo-namespace=$(POD_NAMESPACE)
- --networking-enabled={{ .Values.networking.enabled }}
- --authentication-enabled={{ .Values.authentication.enabled }}
- --tls-compatibility-mode={{ .Values.authentication.tlsCompatibilityMode }}
- --offloading-enabled={{ .Values.offloading.enabled }}
- --default-limits-enforcement={{ .Values.controllerManager.config.defaultLimitsEnforcement }}
{{- $d := dict "commandName" "--default-node-resources" "dictionary" .Values.offloading.defaultNodeResources -}}
Expand Down
6 changes: 6 additions & 0 deletions deployments/liqo/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,12 @@ networking:
authentication:
# -- Enable/Disable the authentication module.
enabled: true
# -- Enable TLS compatibility mode for client certificates and keys.
# If set to true, Liqo will use widely supported algorithm (RSA)
# instead of Ed25519 (default) for generating private keys and CSRs.
# Enable this option to ensure compatibility with systems that do not yet
# support Ed25519 as signature algorithm.
tlsCompatibilityMode: false
# AWS-specific configuration for the local cluster and the Liqo user.
# This user should be able (1) to create new IAM users, (2) to create new programmatic access
# credentials, and (3) to describe EKS clusters.
Expand Down
4 changes: 4 additions & 0 deletions docs/usage/liqoctl/liqoctl_generate.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,10 @@ liqoctl generate peering-user [flags]

>The cluster ID of the cluster from which peering will be performed

`--tls-compatibility-mode` _string_:

>TLS compatibility mode for peering-user keys: one of auto,true,false. If set to true keys are generated with a widely supported algorithm (RSA) to ensure compatibility with systems that do not yet support Ed25519 (default) as signature algorithm. When auto, liqoctl attempts to detect the system configuration. **(default "auto")**


### Global options

Expand Down
55 changes: 37 additions & 18 deletions pkg/liqo-controller-manager/authentication/csr.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,11 @@ package authentication

import (
"bytes"
"crypto"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"crypto/x509/pkix"
Expand All @@ -33,7 +36,7 @@ import (
type CSRChecker func(*x509.CertificateRequest) error

// GenerateCSRForResourceSlice generates a new CSR given a private key and a resource slice.
func GenerateCSRForResourceSlice(key ed25519.PrivateKey,
func GenerateCSRForResourceSlice(key crypto.PrivateKey,
resourceSlice *authv1beta1.ResourceSlice) (csrBytes []byte, err error) {
return generateCSR(key, CommonNameResourceSliceCSR(resourceSlice), OrganizationResourceSliceCSR(resourceSlice))
}
Expand Down Expand Up @@ -64,12 +67,12 @@ func OrganizationResourceSliceCSR(resourceSlice *authv1beta1.ResourceSlice) stri
}

// GenerateCSRForControlPlane generates a new CSR given a private key and a subject.
func GenerateCSRForControlPlane(key ed25519.PrivateKey, clusterID liqov1beta1.ClusterID) (csrBytes []byte, err error) {
func GenerateCSRForControlPlane(key crypto.PrivateKey, clusterID liqov1beta1.ClusterID) (csrBytes []byte, err error) {
return generateCSR(key, CommonNameControlPlaneCSR(clusterID), OrganizationControlPlaneCSR())
}

// GenerateCSRForPeerUser generates a new CSR given a private key and the clusterID from which the peering will start.
func GenerateCSRForPeerUser(key ed25519.PrivateKey, clusterID liqov1beta1.ClusterID) (csrBytes []byte, userCN string, err error) {
func GenerateCSRForPeerUser(key crypto.PrivateKey, clusterID liqov1beta1.ClusterID) (csrBytes []byte, userCN string, err error) {
userCN, err = commonNamePeerUser(clusterID)
if err != nil {
return nil, "", fmt.Errorf("unable to generate user CN: %w", err)
Expand Down Expand Up @@ -162,40 +165,56 @@ func checkCSR(csr, publicKey []byte, checkPublicKey bool, commonName, organizati
}

if checkPublicKey {
// Check the length of the public key and return an error if invalid
// Validate provided public key bytes
if len(publicKey) == 0 {
return fmt.Errorf("invalid public key")
}

// if the pub key is 0-terminated, drop it
if publicKey[len(publicKey)-1] == 0 {
publicKey = publicKey[:len(publicKey)-1]
// Marshal CSR public key to PKIX DER and compare with provided PKIX public key bytes
csrPubDER, err := x509.MarshalPKIXPublicKey(x509Csr.PublicKey)
if err != nil {
return fmt.Errorf("failed to marshal CSR public key: %w", err)
}

// Check that the public key used the expected algorithm and verify that the CSR has been
// signed with the key provided by the peer at peering time.
switch crtKey := x509Csr.PublicKey.(type) {
case ed25519.PublicKey:
if !bytes.Equal(crtKey, publicKey) {
return fmt.Errorf("invalid public key")
}
default:
return fmt.Errorf("invalid public key type %T", crtKey)
if !bytes.Equal(csrPubDER, publicKey) {
return fmt.Errorf("invalid public key")
}
}

return nil
}

func generateCSR(key ed25519.PrivateKey, commonName, organization string) (csrBytes []byte, err error) {
func generateCSR(key crypto.PrivateKey, commonName, organization string) (csrBytes []byte, err error) {
asn1Subj, err := asn1.Marshal(pkix.Name{CommonName: commonName, Organization: []string{organization}}.ToRDNSequence())
if err != nil {
return nil, fmt.Errorf("failed to marshal subject information: %w", err)
}

// Select appropriate signature algorithm based on private key type
var sigAlg x509.SignatureAlgorithm
switch k := key.(type) {
case ed25519.PrivateKey:
sigAlg = x509.PureEd25519
case *rsa.PrivateKey:
// Default to SHA256 for RSA keys
sigAlg = x509.SHA256WithRSA
case *ecdsa.PrivateKey:
// Choose hash based on curve size
switch k.Curve.Params().BitSize {
case 521:
sigAlg = x509.ECDSAWithSHA512
case 384:
sigAlg = x509.ECDSAWithSHA384
default:
sigAlg = x509.ECDSAWithSHA256
}
default:
return nil, fmt.Errorf("unsupported private key type %T", key)
}

template := x509.CertificateRequest{
RawSubject: asn1Subj,
SignatureAlgorithm: x509.PureEd25519,
SignatureAlgorithm: sigAlg,
}

csrBytes, err = x509.CreateCertificateRequest(rand.Reader, &template, key)
Expand Down
104 changes: 90 additions & 14 deletions pkg/liqo-controller-manager/authentication/keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,12 @@ package authentication

import (
"context"
"crypto"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/pem"
"fmt"
Expand Down Expand Up @@ -55,26 +59,100 @@ func GenerateEd25519Keys() (privateKey, publicKey []byte, err error) {
return privateKeyPEM, publicKeyPEM, nil
}

// SignNonce signs a nonce using the provided private key.
func SignNonce(priv ed25519.PrivateKey, nonce []byte) []byte {
return ed25519.Sign(priv, nonce)
// GenerateRSAKeys returns a new pair of RSA private and public keys in PEM format.
// Keys are generated using RSA 2048 bits and encoded in PEM format.
func GenerateRSAKeys() (privateKey, publicKey []byte, err error) {
priv, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, nil, fmt.Errorf("failed to generate RSA private key: %w", err)
}

privateKeyBytes, err := x509.MarshalPKCS8PrivateKey(priv)
if err != nil {
return nil, nil, fmt.Errorf("failed to marshal RSA private key: %w", err)
}
privateKeyPEM := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privateKeyBytes})

publicKeyBytes, err := x509.MarshalPKIXPublicKey(&priv.PublicKey)
if err != nil {
return nil, nil, fmt.Errorf("failed to marshal RSA public key: %w", err)
}
publicKeyPEM := pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: publicKeyBytes})

return privateKeyPEM, publicKeyPEM, nil
}

// VerifyNonce verifies the signature of a nonce using the public key of the cluster.
func VerifyNonce(pubKey ed25519.PublicKey, nonce, signature []byte) bool {
return ed25519.Verify(pubKey, nonce, signature)
// SignNonce signs a nonce using the provided private key. The private key can be
// ed25519.PrivateKey, *rsa.PrivateKey, or *ecdsa.PrivateKey. For RSA/ECDSA the nonce
// is hashed with SHA-256 before signing.
func SignNonce(priv crypto.PrivateKey, nonce []byte) ([]byte, error) {
switch k := priv.(type) {
case ed25519.PrivateKey:
sig := ed25519.Sign(k, nonce)
return sig, nil
case *rsa.PrivateKey:
sum := sha256.Sum256(nonce)
sig, err := rsa.SignPKCS1v15(rand.Reader, k, crypto.SHA256, sum[:])
if err != nil {
return nil, fmt.Errorf("rsa sign failed: %w", err)
}
return sig, nil
case *ecdsa.PrivateKey:
sum := sha256.Sum256(nonce)
sig, err := ecdsa.SignASN1(rand.Reader, k, sum[:])
if err != nil {
return nil, fmt.Errorf("ecdsa sign failed: %w", err)
}
return sig, nil
default:
return nil, fmt.Errorf("unsupported private key type %T", priv)
}
}

// VerifyNonce verifies the signature of a nonce using the PKIX-encoded public key bytes of the cluster.
// The public key can be Ed25519, RSA, or ECDSA.
func VerifyNonce(pubKeyPKIX, nonce, signature []byte) (bool, error) {
pub, err := x509.ParsePKIXPublicKey(pubKeyPKIX)
if err != nil {
return false, fmt.Errorf("failed to parse public key: %w", err)
}

switch pk := pub.(type) {
case ed25519.PublicKey:
return ed25519.Verify(pk, nonce, signature), nil
case *rsa.PublicKey:
sum := sha256.Sum256(nonce)
if err := rsa.VerifyPKCS1v15(pk, crypto.SHA256, sum[:], signature); err != nil {
return false, nil
}
return true, nil
case *ecdsa.PublicKey:
sum := sha256.Sum256(nonce)
if ecdsa.VerifyASN1(pk, sum[:], signature) {
return true, nil
}
return false, nil
default:
return false, fmt.Errorf("unsupported public key type %T", pub)
}
}

// InitClusterKeys initializes the authentication keys for the cluster.
// If the secret containing the keys does not exist, it generates a new pair of keys and stores them in a secret.
func InitClusterKeys(ctx context.Context, cl client.Client, liqoNamespace string) error {
// If tlsCompatibilityMode is true, RSA keys are generated instead of Ed25519.
func InitClusterKeys(ctx context.Context, cl client.Client, liqoNamespace string, tlsCompatibilityMode bool) error {
// Get secret if it exists
var secret corev1.Secret
err := cl.Get(ctx, client.ObjectKey{Name: consts.AuthKeysSecretName, Namespace: liqoNamespace}, &secret)
switch {
case apierrors.IsNotFound(err):
// Forge a new pair of keys.
private, public, err := GenerateEd25519Keys()
var private, public []byte
if tlsCompatibilityMode {
private, public, err = GenerateRSAKeys()
} else {
private, public, err = GenerateEd25519Keys()
}
if err != nil {
return fmt.Errorf("error while generating cluster authentication keys: %w", err)
}
Expand Down Expand Up @@ -107,7 +185,8 @@ func InitClusterKeys(ctx context.Context, cl client.Client, liqoNamespace string
}

// GetClusterKeys retrieves the private and public keys of the cluster from the secret.
func GetClusterKeys(ctx context.Context, cl client.Client, liqoNamespace string) (ed25519.PrivateKey, ed25519.PublicKey, error) {
// It returns the private key as crypto.PrivateKey and the public key as PKIX-encoded bytes.
func GetClusterKeys(ctx context.Context, cl client.Client, liqoNamespace string) (crypto.PrivateKey, []byte, error) {
var secret corev1.Secret
if err := cl.Get(ctx, client.ObjectKey{Name: consts.AuthKeysSecretName, Namespace: liqoNamespace}, &secret); err != nil {
return nil, nil, fmt.Errorf("unable to get secret with cluster authentication keys: %w", err)
Expand All @@ -134,16 +213,13 @@ func GetClusterKeys(ctx context.Context, cl client.Client, liqoNamespace string)
return nil, nil, fmt.Errorf("public key not found in secret %s/%s", liqoNamespace, consts.AuthKeysSecretName)
}

// The public key is stored in PEM format with PKIX-encoded bytes. Return the DER bytes for portability.
publicKeyPEM, _ := pem.Decode(publicKey)
if publicKeyPEM == nil {
return nil, nil, fmt.Errorf("failed to decode public key in PEM format")
}
pub, err := x509.ParsePKIXPublicKey(publicKeyPEM.Bytes)
if err != nil {
return nil, nil, fmt.Errorf("failed to parse public key: %w", err)
}

return priv.(ed25519.PrivateKey), pub.(ed25519.PublicKey), nil
return priv, publicKeyPEM.Bytes, nil
}

// GetClusterKeysPEM retrieves the private and public keys of the cluster from the secret and encoded in PEM format.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,11 @@ func (r *NonceSignerReconciler) Reconcile(ctx context.Context, req ctrl.Request)
}

// Sign the nonce using the private key.
signedNonce := authentication.SignNonce(privateKey, nonce)
signedNonce, err := authentication.SignNonce(privateKey, nonce)
if err != nil {
klog.Errorf("unable to sign nonce for secret %q: %v", req.NamespacedName, err)
return ctrl.Result{}, err
}

// Check if the secret is already signed and the signature is the same.
existingSignedNonce, found := secret.Data[consts.SignedNonceSecretField]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ package tenantcontroller

import (
"context"
"crypto/ed25519"
"fmt"

corev1 "k8s.io/api/core/v1"
Expand Down Expand Up @@ -177,9 +176,14 @@ func (r *TenantReconciler) Reconcile(ctx context.Context, req ctrl.Request) (res
return ctrl.Result{}, err
}

// check the signature

if !authentication.VerifyNonce(ed25519.PublicKey(tenant.Spec.PublicKey), nonce, tenant.Spec.Signature) {
// check the signature using the PKIX-encoded public key bytes
ok, err := authentication.VerifyNonce(tenant.Spec.PublicKey, nonce, tenant.Spec.Signature)
if err != nil {
klog.Errorf("Unable to verify signature for Tenant %q: %s", req.Name, err)
r.EventRecorder.Event(tenant, corev1.EventTypeWarning, "SignatureVerificationFailed", err.Error())
return ctrl.Result{}, err
}
if !ok {
err = fmt.Errorf("signature verification failed for Tenant %q", req.Name)
klog.Error(err)
r.EventRecorder.Event(tenant, corev1.EventTypeWarning, "SignatureVerificationFailed", err.Error())
Expand Down
2 changes: 2 additions & 0 deletions pkg/liqo-controller-manager/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ func InitFlags(flagset *pflag.FlagSet, opts *Options) {
"Override the API server address where the Kuberentes APIServer is exposed")
flagset.StringVar(&opts.CAOverride, "ca-override", "", "Override the CA certificate used by Kubernetes to sign certificates (base64 encoded)")
flagset.BoolVar(&opts.TrustedCA, "trusted-ca", false, "Whether the Kubernetes APIServer certificate is issue by a trusted CA")
flagset.BoolVar(&opts.TLSCompatibilityMode, "tls-compatibility-mode", false,
"Enable TLS compatibility mode for client certificates and keys (use RSA instead of Ed25519)")
flagset.StringVar(&opts.AWSConfig.AwsAccessKeyID, "aws-access-key-id", "", "AWS IAM AccessKeyID for the Liqo User")
flagset.StringVar(&opts.AWSConfig.AwsSecretAccessKey, "aws-secret-access-key", "", "AWS IAM SecretAccessKey for the Liqo User")
flagset.StringVar(&opts.AWSConfig.AwsRegion, "aws-region", "", "AWS region where the local cluster is running")
Expand Down
1 change: 1 addition & 0 deletions pkg/liqo-controller-manager/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ type Options struct {
APIServerAddressOverride string
CAOverride string
TrustedCA bool
TLSCompatibilityMode bool
AWSConfig *identitymanager.LocalAwsConfig
ClusterLabels args.StringMap
IngressClasses args.ClassNameList
Expand Down
Loading