Skip to content

Commit 71584b8

Browse files
aleoliadamjensenbot
authored andcommitted
feat(auth): add TLS compatibility mode for authentication keys
1 parent a69ef28 commit 71584b8

File tree

14 files changed

+225
-46
lines changed

14 files changed

+225
-46
lines changed

cmd/liqo-controller-manager/modules/authentication.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ type AuthOption struct {
4747
APIServerAddressOverride string
4848
CAOverrideB64 string
4949
TrustedCA bool
50+
TLSCompatibilityMode bool
5051
SliceStatusOptions *remoteresourceslicecontroller.SliceStatusOptions
5152
}
5253

@@ -61,6 +62,7 @@ func NewAuthOption(identityProvider identitymanager.IdentityProvider, namespaceM
6162
APIServerAddressOverride: opts.APIServerAddressOverride,
6263
CAOverrideB64: opts.CAOverride,
6364
TrustedCA: opts.TrustedCA,
65+
TLSCompatibilityMode: opts.TLSCompatibilityMode,
6466
SliceStatusOptions: &remoteresourceslicecontroller.SliceStatusOptions{
6567
EnableStorage: opts.EnableStorage,
6668
LocalRealStorageClassName: opts.RealStorageClassName,
@@ -85,7 +87,7 @@ func SetupAuthenticationModule(ctx context.Context, mgr manager.Manager, uncache
8587
}
8688
}
8789

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

181-
func enforceAuthenticationKeys(ctx context.Context, cl client.Client, liqoNamespace string) error {
182-
if err := authentication.InitClusterKeys(ctx, cl, liqoNamespace); err != nil {
183+
func enforceAuthenticationKeys(ctx context.Context, cl client.Client, liqoNamespace string, tlsCompatibilityMode bool) error {
184+
if err := authentication.InitClusterKeys(ctx, cl, liqoNamespace, tlsCompatibilityMode); err != nil {
183185
return err
184186
}
185187

deployments/liqo/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
| authentication.awsConfig.secretAccessKey | string | `""` | SecretAccessKey for the Liqo user. |
1212
| authentication.awsConfig.useExistingSecret | bool | `false` | Use an existing secret to configure the AWS credentials. |
1313
| authentication.enabled | bool | `true` | Enable/Disable the authentication module. |
14+
| 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. |
1415
| common.affinity | object | `{}` | Affinity for all liqo pods, excluding virtual kubelet. |
1516
| common.extraArgs | list | `[]` | Extra arguments for all liqo pods, excluding virtual kubelet. |
1617
| common.globalAnnotations | object | `{}` | Global annotations to be added to all resources created by Liqo controllers |

deployments/liqo/templates/liqo-controller-manager-deployment.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ spec:
5252
- --liqo-namespace=$(POD_NAMESPACE)
5353
- --networking-enabled={{ .Values.networking.enabled }}
5454
- --authentication-enabled={{ .Values.authentication.enabled }}
55+
- --tls-compatibility-mode={{ .Values.authentication.tlsCompatibilityMode }}
5556
- --offloading-enabled={{ .Values.offloading.enabled }}
5657
- --default-limits-enforcement={{ .Values.controllerManager.config.defaultLimitsEnforcement }}
5758
{{- $d := dict "commandName" "--default-node-resources" "dictionary" .Values.offloading.defaultNodeResources -}}

deployments/liqo/values.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,12 @@ networking:
141141
authentication:
142142
# -- Enable/Disable the authentication module.
143143
enabled: true
144+
# -- Enable TLS compatibility mode for client certificates and keys.
145+
# If set to true, Liqo will use widely supported algorithm (RSA)
146+
# instead of Ed25519 (default) for generating private keys and CSRs.
147+
# Enable this option to ensure compatibility with systems that do not yet
148+
# support Ed25519 as signature algorithm.
149+
tlsCompatibilityMode: false
144150
# AWS-specific configuration for the local cluster and the Liqo user.
145151
# This user should be able (1) to create new IAM users, (2) to create new programmatic access
146152
# credentials, and (3) to describe EKS clusters.

docs/usage/liqoctl/liqoctl_generate.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,10 @@ liqoctl generate peering-user [flags]
182182

183183
>The cluster ID of the cluster from which peering will be performed
184184
185+
`--tls-compatibility-mode` _string_:
186+
187+
>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")**
188+
185189

186190
### Global options
187191

pkg/liqo-controller-manager/authentication/csr.go

Lines changed: 37 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,11 @@ package authentication
1616

1717
import (
1818
"bytes"
19+
"crypto"
20+
"crypto/ecdsa"
1921
"crypto/ed25519"
2022
"crypto/rand"
23+
"crypto/rsa"
2124
"crypto/sha256"
2225
"crypto/x509"
2326
"crypto/x509/pkix"
@@ -33,7 +36,7 @@ import (
3336
type CSRChecker func(*x509.CertificateRequest) error
3437

3538
// GenerateCSRForResourceSlice generates a new CSR given a private key and a resource slice.
36-
func GenerateCSRForResourceSlice(key ed25519.PrivateKey,
39+
func GenerateCSRForResourceSlice(key crypto.PrivateKey,
3740
resourceSlice *authv1beta1.ResourceSlice) (csrBytes []byte, err error) {
3841
return generateCSR(key, CommonNameResourceSliceCSR(resourceSlice), OrganizationResourceSliceCSR(resourceSlice))
3942
}
@@ -64,12 +67,12 @@ func OrganizationResourceSliceCSR(resourceSlice *authv1beta1.ResourceSlice) stri
6467
}
6568

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

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

164167
if checkPublicKey {
165-
// Check the length of the public key and return an error if invalid
168+
// Validate provided public key bytes
166169
if len(publicKey) == 0 {
167170
return fmt.Errorf("invalid public key")
168171
}
169172

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

175-
// Check that the public key used the expected algorithm and verify that the CSR has been
176-
// signed with the key provided by the peer at peering time.
177-
switch crtKey := x509Csr.PublicKey.(type) {
178-
case ed25519.PublicKey:
179-
if !bytes.Equal(crtKey, publicKey) {
180-
return fmt.Errorf("invalid public key")
181-
}
182-
default:
183-
return fmt.Errorf("invalid public key type %T", crtKey)
179+
if !bytes.Equal(csrPubDER, publicKey) {
180+
return fmt.Errorf("invalid public key")
184181
}
185182
}
186183

187184
return nil
188185
}
189186

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

193+
// Select appropriate signature algorithm based on private key type
194+
var sigAlg x509.SignatureAlgorithm
195+
switch k := key.(type) {
196+
case ed25519.PrivateKey:
197+
sigAlg = x509.PureEd25519
198+
case *rsa.PrivateKey:
199+
// Default to SHA256 for RSA keys
200+
sigAlg = x509.SHA256WithRSA
201+
case *ecdsa.PrivateKey:
202+
// Choose hash based on curve size
203+
switch k.Curve.Params().BitSize {
204+
case 521:
205+
sigAlg = x509.ECDSAWithSHA512
206+
case 384:
207+
sigAlg = x509.ECDSAWithSHA384
208+
default:
209+
sigAlg = x509.ECDSAWithSHA256
210+
}
211+
default:
212+
return nil, fmt.Errorf("unsupported private key type %T", key)
213+
}
214+
196215
template := x509.CertificateRequest{
197216
RawSubject: asn1Subj,
198-
SignatureAlgorithm: x509.PureEd25519,
217+
SignatureAlgorithm: sigAlg,
199218
}
200219

201220
csrBytes, err = x509.CreateCertificateRequest(rand.Reader, &template, key)

pkg/liqo-controller-manager/authentication/keys.go

Lines changed: 90 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,12 @@ package authentication
1616

1717
import (
1818
"context"
19+
"crypto"
20+
"crypto/ecdsa"
1921
"crypto/ed25519"
2022
"crypto/rand"
23+
"crypto/rsa"
24+
"crypto/sha256"
2125
"crypto/x509"
2226
"encoding/pem"
2327
"fmt"
@@ -55,26 +59,100 @@ func GenerateEd25519Keys() (privateKey, publicKey []byte, err error) {
5559
return privateKeyPEM, publicKeyPEM, nil
5660
}
5761

58-
// SignNonce signs a nonce using the provided private key.
59-
func SignNonce(priv ed25519.PrivateKey, nonce []byte) []byte {
60-
return ed25519.Sign(priv, nonce)
62+
// GenerateRSAKeys returns a new pair of RSA private and public keys in PEM format.
63+
// Keys are generated using RSA 2048 bits and encoded in PEM format.
64+
func GenerateRSAKeys() (privateKey, publicKey []byte, err error) {
65+
priv, err := rsa.GenerateKey(rand.Reader, 2048)
66+
if err != nil {
67+
return nil, nil, fmt.Errorf("failed to generate RSA private key: %w", err)
68+
}
69+
70+
privateKeyBytes, err := x509.MarshalPKCS8PrivateKey(priv)
71+
if err != nil {
72+
return nil, nil, fmt.Errorf("failed to marshal RSA private key: %w", err)
73+
}
74+
privateKeyPEM := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privateKeyBytes})
75+
76+
publicKeyBytes, err := x509.MarshalPKIXPublicKey(&priv.PublicKey)
77+
if err != nil {
78+
return nil, nil, fmt.Errorf("failed to marshal RSA public key: %w", err)
79+
}
80+
publicKeyPEM := pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: publicKeyBytes})
81+
82+
return privateKeyPEM, publicKeyPEM, nil
6183
}
6284

63-
// VerifyNonce verifies the signature of a nonce using the public key of the cluster.
64-
func VerifyNonce(pubKey ed25519.PublicKey, nonce, signature []byte) bool {
65-
return ed25519.Verify(pubKey, nonce, signature)
85+
// SignNonce signs a nonce using the provided private key. The private key can be
86+
// ed25519.PrivateKey, *rsa.PrivateKey, or *ecdsa.PrivateKey. For RSA/ECDSA the nonce
87+
// is hashed with SHA-256 before signing.
88+
func SignNonce(priv crypto.PrivateKey, nonce []byte) ([]byte, error) {
89+
switch k := priv.(type) {
90+
case ed25519.PrivateKey:
91+
sig := ed25519.Sign(k, nonce)
92+
return sig, nil
93+
case *rsa.PrivateKey:
94+
sum := sha256.Sum256(nonce)
95+
sig, err := rsa.SignPKCS1v15(rand.Reader, k, crypto.SHA256, sum[:])
96+
if err != nil {
97+
return nil, fmt.Errorf("rsa sign failed: %w", err)
98+
}
99+
return sig, nil
100+
case *ecdsa.PrivateKey:
101+
sum := sha256.Sum256(nonce)
102+
sig, err := ecdsa.SignASN1(rand.Reader, k, sum[:])
103+
if err != nil {
104+
return nil, fmt.Errorf("ecdsa sign failed: %w", err)
105+
}
106+
return sig, nil
107+
default:
108+
return nil, fmt.Errorf("unsupported private key type %T", priv)
109+
}
110+
}
111+
112+
// VerifyNonce verifies the signature of a nonce using the PKIX-encoded public key bytes of the cluster.
113+
// The public key can be Ed25519, RSA, or ECDSA.
114+
func VerifyNonce(pubKeyPKIX, nonce, signature []byte) (bool, error) {
115+
pub, err := x509.ParsePKIXPublicKey(pubKeyPKIX)
116+
if err != nil {
117+
return false, fmt.Errorf("failed to parse public key: %w", err)
118+
}
119+
120+
switch pk := pub.(type) {
121+
case ed25519.PublicKey:
122+
return ed25519.Verify(pk, nonce, signature), nil
123+
case *rsa.PublicKey:
124+
sum := sha256.Sum256(nonce)
125+
if err := rsa.VerifyPKCS1v15(pk, crypto.SHA256, sum[:], signature); err != nil {
126+
return false, nil
127+
}
128+
return true, nil
129+
case *ecdsa.PublicKey:
130+
sum := sha256.Sum256(nonce)
131+
if ecdsa.VerifyASN1(pk, sum[:], signature) {
132+
return true, nil
133+
}
134+
return false, nil
135+
default:
136+
return false, fmt.Errorf("unsupported public key type %T", pub)
137+
}
66138
}
67139

68140
// InitClusterKeys initializes the authentication keys for the cluster.
69141
// If the secret containing the keys does not exist, it generates a new pair of keys and stores them in a secret.
70-
func InitClusterKeys(ctx context.Context, cl client.Client, liqoNamespace string) error {
142+
// If tlsCompatibilityMode is true, RSA keys are generated instead of Ed25519.
143+
func InitClusterKeys(ctx context.Context, cl client.Client, liqoNamespace string, tlsCompatibilityMode bool) error {
71144
// Get secret if it exists
72145
var secret corev1.Secret
73146
err := cl.Get(ctx, client.ObjectKey{Name: consts.AuthKeysSecretName, Namespace: liqoNamespace}, &secret)
74147
switch {
75148
case apierrors.IsNotFound(err):
76149
// Forge a new pair of keys.
77-
private, public, err := GenerateEd25519Keys()
150+
var private, public []byte
151+
if tlsCompatibilityMode {
152+
private, public, err = GenerateRSAKeys()
153+
} else {
154+
private, public, err = GenerateEd25519Keys()
155+
}
78156
if err != nil {
79157
return fmt.Errorf("error while generating cluster authentication keys: %w", err)
80158
}
@@ -107,7 +185,8 @@ func InitClusterKeys(ctx context.Context, cl client.Client, liqoNamespace string
107185
}
108186

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

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

146-
return priv.(ed25519.PrivateKey), pub.(ed25519.PublicKey), nil
222+
return priv, publicKeyPEM.Bytes, nil
147223
}
148224

149225
// GetClusterKeysPEM retrieves the private and public keys of the cluster from the secret and encoded in PEM format.

pkg/liqo-controller-manager/authentication/noncesigner-controller/noncesigner_controller.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,11 @@ func (r *NonceSignerReconciler) Reconcile(ctx context.Context, req ctrl.Request)
116116
}
117117

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

121125
// Check if the secret is already signed and the signature is the same.
122126
existingSignedNonce, found := secret.Data[consts.SignedNonceSecretField]

pkg/liqo-controller-manager/authentication/tenant-controller/tenant_controller.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ package tenantcontroller
1616

1717
import (
1818
"context"
19-
"crypto/ed25519"
2019
"fmt"
2120

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

180-
// check the signature
181-
182-
if !authentication.VerifyNonce(ed25519.PublicKey(tenant.Spec.PublicKey), nonce, tenant.Spec.Signature) {
179+
// check the signature using the PKIX-encoded public key bytes
180+
ok, err := authentication.VerifyNonce(tenant.Spec.PublicKey, nonce, tenant.Spec.Signature)
181+
if err != nil {
182+
klog.Errorf("Unable to verify signature for Tenant %q: %s", req.Name, err)
183+
r.EventRecorder.Event(tenant, corev1.EventTypeWarning, "SignatureVerificationFailed", err.Error())
184+
return ctrl.Result{}, err
185+
}
186+
if !ok {
183187
err = fmt.Errorf("signature verification failed for Tenant %q", req.Name)
184188
klog.Error(err)
185189
r.EventRecorder.Event(tenant, corev1.EventTypeWarning, "SignatureVerificationFailed", err.Error())

pkg/liqo-controller-manager/flags.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ func InitFlags(flagset *pflag.FlagSet, opts *Options) {
8383
"Override the API server address where the Kuberentes APIServer is exposed")
8484
flagset.StringVar(&opts.CAOverride, "ca-override", "", "Override the CA certificate used by Kubernetes to sign certificates (base64 encoded)")
8585
flagset.BoolVar(&opts.TrustedCA, "trusted-ca", false, "Whether the Kubernetes APIServer certificate is issue by a trusted CA")
86+
flagset.BoolVar(&opts.TLSCompatibilityMode, "tls-compatibility-mode", false,
87+
"Enable TLS compatibility mode for client certificates and keys (use RSA instead of Ed25519)")
8688
flagset.StringVar(&opts.AWSConfig.AwsAccessKeyID, "aws-access-key-id", "", "AWS IAM AccessKeyID for the Liqo User")
8789
flagset.StringVar(&opts.AWSConfig.AwsSecretAccessKey, "aws-secret-access-key", "", "AWS IAM SecretAccessKey for the Liqo User")
8890
flagset.StringVar(&opts.AWSConfig.AwsRegion, "aws-region", "", "AWS region where the local cluster is running")

0 commit comments

Comments
 (0)