Skip to content

Commit 492a8bc

Browse files
committed
feat(auth): add TLS compatibility mode for authentication keys
1 parent 525e6ef commit 492a8bc

File tree

13 files changed

+214
-46
lines changed

13 files changed

+214
-46
lines changed

cmd/liqo-controller-manager/main.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,8 @@ func main() {
143143
"api-server-address-override", "", "Override the API server address where the Kuberentes APIServer is exposed")
144144
pflag.StringVar(&caOverride, "ca-override", "", "Override the CA certificate used by Kubernetes to sign certificates (base64 encoded)")
145145
pflag.BoolVar(&trustedCA, "trusted-ca", false, "Whether the Kubernetes APIServer certificate is issue by a trusted CA")
146+
// TLS compatibility mode
147+
tlsCompatibilityMode := pflag.Bool("tls-compatibility-mode", false, "Enable TLS compatibility mode for client certificates and keys (use RSA/ECDSA instead of Ed25519)")
146148
// AWS configurations
147149
pflag.StringVar(&awsConfig.AwsAccessKeyID, "aws-access-key-id", "", "AWS IAM AccessKeyID for the Liqo User")
148150
pflag.StringVar(&awsConfig.AwsSecretAccessKey, "aws-secret-access-key", "", "AWS IAM SecretAccessKey for the Liqo User")
@@ -324,6 +326,7 @@ func main() {
324326
APIServerAddressOverride: apiServerAddressOverride,
325327
CAOverrideB64: caOverride,
326328
TrustedCA: trustedCA,
329+
TLSCompatibilityMode: *tlsCompatibilityMode,
327330
SliceStatusOptions: &remoteresourceslicecontroller.SliceStatusOptions{
328331
EnableStorage: *enableStorage,
329332
LocalRealStorageClassName: *realStorageClassName,

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ type AuthOption struct {
4646
APIServerAddressOverride string
4747
CAOverrideB64 string
4848
TrustedCA bool
49+
TLSCompatibilityMode bool
4950
SliceStatusOptions *remoteresourceslicecontroller.SliceStatusOptions
5051
}
5152

@@ -62,7 +63,7 @@ func SetupAuthenticationModule(ctx context.Context, mgr manager.Manager, uncache
6263
}
6364
}
6465

65-
if err := enforceAuthenticationKeys(ctx, uncachedClient, opts.LiqoNamespace); err != nil {
66+
if err := enforceAuthenticationKeys(ctx, uncachedClient, opts.LiqoNamespace, opts.TLSCompatibilityMode); err != nil {
6667
klog.Errorf("Unable to enforce authentication keys: %v", err)
6768
return err
6869
}
@@ -155,8 +156,8 @@ func SetupAuthenticationModule(ctx context.Context, mgr manager.Manager, uncache
155156
return nil
156157
}
157158

158-
func enforceAuthenticationKeys(ctx context.Context, cl client.Client, liqoNamespace string) error {
159-
if err := authentication.InitClusterKeys(ctx, cl, liqoNamespace); err != nil {
159+
func enforceAuthenticationKeys(ctx context.Context, cl client.Client, liqoNamespace string, tlsCompatibilityMode bool) error {
160+
if err := authentication.InitClusterKeys(ctx, cl, liqoNamespace, tlsCompatibilityMode); err != nil {
160161
return err
161162
}
162163

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. When true, Liqo will use a widely supported algorithm (e.g., RSA/ECDSA) for generating private keys and CSRs instead of Ed25519. Keep disabled by default to preserve current behavior. |
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: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,11 @@ networking:
141141
authentication:
142142
# -- Enable/Disable the authentication module.
143143
enabled: true
144+
# -- Enable TLS compatibility mode for client certificates and keys.
145+
# When true, Liqo will use a widely supported algorithm (e.g., RSA/ECDSA)
146+
# for generating private keys and CSRs instead of Ed25519. Keep disabled
147+
# by default to preserve current behavior.
148+
tlsCompatibilityMode: false
144149
# AWS-specific configuration for the local cluster and the Liqo user.
145150
# This user should be able (1) to create new IAM users, (2) to create new programmatic access
146151
# 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. When auto, liqoctl attempts to detect the controller manager setting. **(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+
sigAlg := x509.UnknownSignatureAlgorithm
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 []byte, 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())

0 commit comments

Comments
 (0)