Skip to content

Commit 6bdfab6

Browse files
committed
feat: enable self registration of agents
Assisted by: Cursor Signed-off-by: Jayendra Parsai <jparsai@redhat.com>
1 parent 2c170ba commit 6bdfab6

File tree

11 files changed

+263
-75
lines changed

11 files changed

+263
-75
lines changed

cmd/argocd-agent/principal.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,8 @@ func NewPrincipalRunCommand() *cobra.Command {
9999
otlpAddress string
100100
otlpInsecure bool
101101

102-
enableSelfClusterRegistration bool
102+
enableSelfClusterRegistration bool
103+
selfClusterRegistrationSharedCert string
103104
)
104105
command := &cobra.Command{
105106
Use: "principal",
@@ -330,6 +331,9 @@ func NewPrincipalRunCommand() *cobra.Command {
330331

331332
// Self cluster registration options
332333
opts = append(opts, principal.WithClusterRegistration(enableSelfClusterRegistration))
334+
if selfClusterRegistrationSharedCert != "" {
335+
opts = append(opts, principal.WithSelfClusterRegistrationSharedCert(selfClusterRegistrationSharedCert))
336+
}
333337

334338
s, err := principal.NewServer(ctx, kubeConfig, namespace, opts...)
335339
if err != nil {
@@ -491,6 +495,10 @@ func NewPrincipalRunCommand() *cobra.Command {
491495
env.BoolWithDefault("ARGOCD_PRINCIPAL_ENABLE_SELF_CLUSTER_REGISTRATION", false),
492496
"Allow agents with valid client certificates to self-register on connection")
493497

498+
command.Flags().StringVar(&selfClusterRegistrationSharedCert, "self-cluster-registration-shared-cert",
499+
env.StringWithDefault("ARGOCD_PRINCIPAL_SELF_CLUSTER_REGISTRATION_SHARED_CERT", nil, ""),
500+
"Name of the TLS secret containing the shared client certificate for cluster secrets (requires ca.crt, tls.crt, tls.key)")
501+
494502
return command
495503
}
496504

internal/argocd/cluster/cluster.go

Lines changed: 58 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -192,12 +192,32 @@ func NewClusterCacheInstance(redisAddress, redisPassword string, redisCompressio
192192
}
193193

194194
// CreateCluster creates a cluster secret for an agent's cluster on the principal.
195-
func CreateCluster(ctx context.Context, kubeclient kubernetes.Interface, namespace, agentName, resourceProxyAddress string) error {
195+
// If sharedClientCertSecretName is provided, it reads the TLS credentials from that secret
196+
// (shared client cert mode). Otherwise, it generates a new client certificate signed
197+
// by the principal's CA (legacy mode, requires CA private key).
198+
func CreateCluster(ctx context.Context, kubeclient kubernetes.Interface, namespace, agentName, resourceProxyAddress, sharedClientCertSecretName string) error {
199+
var clientCert, clientKey, caData string
200+
var err error
201+
202+
if sharedClientCertSecretName != "" {
203+
// Shared client cert mode: read from existing secret
204+
log().WithFields(logrus.Fields{
205+
"agent": agentName,
206+
"secret": sharedClientCertSecretName,
207+
}).Info("Using shared client certificate mode for cluster registration")
208+
209+
clientCert, clientKey, caData, err = readSharedClientCertFromSecret(ctx, kubeclient, namespace, sharedClientCertSecretName)
210+
if err != nil {
211+
return fmt.Errorf("could not read shared client certificate from secret %s: %v", sharedClientCertSecretName, err)
212+
}
213+
} else {
214+
// Legacy mode: generate client certificate signed by principal's CA
215+
log().WithField("agent", agentName).Info("Generating unique client certificate for cluster registration")
196216

197-
// Generate client certificate signed by principal's CA.
198-
clientCert, clientKey, caData, err := generateAgentClientCert(ctx, kubeclient, namespace, agentName, config.SecretNamePrincipalCA)
199-
if err != nil {
200-
return fmt.Errorf("could not generate client certificate: %w", err)
217+
clientCert, clientKey, caData, err = generateAgentClientCert(ctx, kubeclient, namespace, agentName, config.SecretNamePrincipalCA)
218+
if err != nil {
219+
return fmt.Errorf("could not generate client certificate: %v", err)
220+
}
201221
}
202222

203223
// Note: this structure has to be same as manual creation done by `argocd-agentctl agent create <agent_name>`
@@ -225,22 +245,47 @@ func CreateCluster(ctx context.Context, kubeclient kubernetes.Interface, namespa
225245
},
226246
}
227247
if err := ClusterToSecret(cluster, secret); err != nil {
228-
return fmt.Errorf("could not convert cluster to secret: %w", err)
248+
return fmt.Errorf("could not convert cluster to secret: %v", err)
229249
}
230250

231251
// Create the secret to register the agent's cluster.
232-
// Handle AlreadyExists as success to make this idempotent and avoid TOCTOU race
233-
// conditions where concurrent registrations both pass the existence check.
234252
if _, err = kubeclient.CoreV1().Secrets(namespace).Create(ctx, secret, metav1.CreateOptions{}); err != nil {
235253
if apierrors.IsAlreadyExists(err) {
236254
return nil
237255
}
238-
return fmt.Errorf("could not create cluster secret to register agent's cluster: %w", err)
256+
return fmt.Errorf("could not create cluster secret to register agent's cluster: %v", err)
239257
}
240258

241259
return nil
242260
}
243261

262+
// readSharedClientCertFromSecret reads TLS credentials from an existing Kubernetes TLS secret.
263+
// The secret should contain tls.crt, tls.key, and ca.crt keys.
264+
func readSharedClientCertFromSecret(ctx context.Context, kubeclient kubernetes.Interface, namespace, sharedClientCertSecretName string) (clientCert, clientKey, caData string, err error) {
265+
secret, err := kubeclient.CoreV1().Secrets(namespace).Get(ctx, sharedClientCertSecretName, metav1.GetOptions{})
266+
if err != nil {
267+
return "", "", "", fmt.Errorf("could not read TLS secret %s/%s: %v", namespace, sharedClientCertSecretName, err)
268+
}
269+
270+
certData, ok := secret.Data["tls.crt"]
271+
if !ok {
272+
return "", "", "", fmt.Errorf("secret %s/%s missing tls.crt", namespace, sharedClientCertSecretName)
273+
}
274+
275+
keyData, ok := secret.Data["tls.key"]
276+
if !ok {
277+
return "", "", "", fmt.Errorf("secret %s/%s missing tls.key", namespace, sharedClientCertSecretName)
278+
}
279+
280+
// CA cert can be in ca.crt (cert-manager style) or tls.crt of a separate CA secret
281+
caBytes, ok := secret.Data["ca.crt"]
282+
if !ok {
283+
return "", "", "", fmt.Errorf("secret %s/%s missing ca.crt", namespace, sharedClientCertSecretName)
284+
}
285+
286+
return string(certData), string(keyData), string(caBytes), nil
287+
}
288+
244289
// ClusterSecretExists checks if a cluster secret exists for the given agent.
245290
func ClusterSecretExists(ctx context.Context, kubeclient kubernetes.Interface, namespace, agentName string) (bool, error) {
246291
if _, err := kubeclient.CoreV1().Secrets(namespace).Get(ctx, getClusterSecretName(agentName), metav1.GetOptions{}); err != nil {
@@ -257,28 +302,28 @@ func generateAgentClientCert(ctx context.Context, kubeclient kubernetes.Interfac
257302
// Read the CA certificate from the principal's CA secret
258303
tlsCert, err := tlsutil.TLSCertFromSecret(ctx, kubeclient, namespace, caSecretName)
259304
if err != nil {
260-
err = fmt.Errorf("could not read CA secret: %w", err)
305+
err = fmt.Errorf("could not read CA secret: %v", err)
261306
return
262307
}
263308

264309
// Parse CA certificate from PEM format
265310
signerCert, err := x509.ParseCertificate(tlsCert.Certificate[0])
266311
if err != nil {
267-
err = fmt.Errorf("could not parse CA certificate: %w", err)
312+
err = fmt.Errorf("could not parse CA certificate: %v", err)
268313
return
269314
}
270315

271316
// Generate a client cert with agent name as CN and sign it with the CA's cert and key
272317
clientCert, clientKey, err = tlsutil.GenerateClientCertificate(agentName, signerCert, tlsCert.PrivateKey)
273318
if err != nil {
274-
err = fmt.Errorf("could not create client cert: %w", err)
319+
err = fmt.Errorf("could not create client cert: %v", err)
275320
return
276321
}
277322

278323
// Convert CA certificate to PEM format
279324
caData, err = tlsutil.CertDataToPEM(tlsCert.Certificate[0])
280325
if err != nil {
281-
err = fmt.Errorf("could not convert CA certificate to PEM format: %w", err)
326+
err = fmt.Errorf("could not convert CA certificate to PEM format: %v", err)
282327
return
283328
}
284329

internal/argocd/cluster/cluster_test.go

Lines changed: 122 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -345,7 +345,7 @@ func Test_CreateCluster(t *testing.T) {
345345
t.Run("Returns error when CA secret is missing", func(t *testing.T) {
346346
kubeclient := kube.NewFakeClientsetWithResources()
347347

348-
err := CreateCluster(context.Background(), kubeclient, testNamespace, "test-agent", testResourceProxyAddr)
348+
err := CreateCluster(context.Background(), kubeclient, testNamespace, "test-agent", testResourceProxyAddr, "")
349349

350350
require.Error(t, err)
351351
require.Contains(t, err.Error(), "could not generate client certificate")
@@ -356,7 +356,7 @@ func Test_CreateCluster(t *testing.T) {
356356
createTestCASecret(t, kubeclient, testNamespace)
357357

358358
agentName := "test-agent"
359-
err := CreateCluster(context.Background(), kubeclient, testNamespace, agentName, testResourceProxyAddr)
359+
err := CreateCluster(context.Background(), kubeclient, testNamespace, agentName, testResourceProxyAddr, "")
360360

361361
require.NoError(t, err)
362362

@@ -371,4 +371,124 @@ func Test_CreateCluster(t *testing.T) {
371371
require.Equal(t, agentName, secret.Labels[LabelKeyClusterAgentMapping])
372372
require.Equal(t, "true", secret.Labels[LabelKeySelfRegisteredCluster])
373373
})
374+
375+
t.Run("Creates cluster secret with shared client cert", func(t *testing.T) {
376+
kubeclient := kube.NewFakeClientsetWithResources()
377+
378+
// Create the shared client cert secret
379+
sharedSecret := &corev1.Secret{
380+
ObjectMeta: metav1.ObjectMeta{
381+
Name: "shared-client-cert",
382+
Namespace: testNamespace,
383+
},
384+
Data: map[string][]byte{
385+
"tls.crt": []byte("shared-cert-data"),
386+
"tls.key": []byte("shared-key-data"),
387+
"ca.crt": []byte("shared-ca-data"),
388+
},
389+
}
390+
_, err := kubeclient.CoreV1().Secrets(testNamespace).Create(context.Background(), sharedSecret, metav1.CreateOptions{})
391+
require.NoError(t, err)
392+
393+
agentName := "test-agent-shared"
394+
err = CreateCluster(context.Background(), kubeclient, testNamespace, agentName, testResourceProxyAddr, "shared-client-cert")
395+
396+
require.NoError(t, err)
397+
398+
// Verify the cluster secret was created with shared cert data
399+
secret, err := kubeclient.CoreV1().Secrets(testNamespace).Get(
400+
context.Background(),
401+
getClusterSecretName(agentName),
402+
metav1.GetOptions{},
403+
)
404+
require.NoError(t, err)
405+
require.NotNil(t, secret)
406+
407+
// Verify the cluster secret contains the shared cert data (base64 encoded in JSON)
408+
clusterData, ok := secret.Data["config"]
409+
require.True(t, ok)
410+
require.Contains(t, string(clusterData), "c2hhcmVkLWNlcnQtZGF0YQ==")
411+
require.Contains(t, string(clusterData), "c2hhcmVkLWtleS1kYXRh")
412+
require.Contains(t, string(clusterData), "c2hhcmVkLWNhLWRhdGE=")
413+
})
414+
415+
t.Run("Returns error when shared client cert secret does not exist", func(t *testing.T) {
416+
kubeclient := kube.NewFakeClientsetWithResources()
417+
418+
err := CreateCluster(context.Background(), kubeclient, testNamespace, "test-agent", testResourceProxyAddr, "non-existent-secret")
419+
420+
require.Error(t, err)
421+
require.Contains(t, err.Error(), "could not read shared client certificate from secret")
422+
})
423+
}
424+
425+
func Test_CreateClusterWithSharedCert(t *testing.T) {
426+
const testNamespace = "argocd"
427+
const testResourceProxyAddr = "resource-proxy:8443"
428+
429+
t.Run("Returns error when shared secret is missing tls.crt", func(t *testing.T) {
430+
kubeclient := kube.NewFakeClientsetWithResources()
431+
432+
secret := &corev1.Secret{
433+
ObjectMeta: metav1.ObjectMeta{
434+
Name: "missing-cert",
435+
Namespace: testNamespace,
436+
},
437+
Data: map[string][]byte{
438+
"tls.key": []byte("key-data"),
439+
"ca.crt": []byte("ca-data"),
440+
},
441+
}
442+
_, err := kubeclient.CoreV1().Secrets(testNamespace).Create(context.Background(), secret, metav1.CreateOptions{})
443+
require.NoError(t, err)
444+
445+
err = CreateCluster(context.Background(), kubeclient, testNamespace, "test-agent", testResourceProxyAddr, "missing-cert")
446+
447+
require.Error(t, err)
448+
require.Contains(t, err.Error(), "missing tls.crt")
449+
})
450+
451+
t.Run("Returns error when shared secret is missing tls.key", func(t *testing.T) {
452+
kubeclient := kube.NewFakeClientsetWithResources()
453+
454+
secret := &corev1.Secret{
455+
ObjectMeta: metav1.ObjectMeta{
456+
Name: "missing-key",
457+
Namespace: testNamespace,
458+
},
459+
Data: map[string][]byte{
460+
"tls.crt": []byte("cert-data"),
461+
"ca.crt": []byte("ca-data"),
462+
},
463+
}
464+
_, err := kubeclient.CoreV1().Secrets(testNamespace).Create(context.Background(), secret, metav1.CreateOptions{})
465+
require.NoError(t, err)
466+
467+
err = CreateCluster(context.Background(), kubeclient, testNamespace, "test-agent", testResourceProxyAddr, "missing-key")
468+
469+
require.Error(t, err)
470+
require.Contains(t, err.Error(), "missing tls.key")
471+
})
472+
473+
t.Run("Returns error when shared secret is missing ca.crt", func(t *testing.T) {
474+
kubeclient := kube.NewFakeClientsetWithResources()
475+
476+
secret := &corev1.Secret{
477+
ObjectMeta: metav1.ObjectMeta{
478+
Name: "missing-ca",
479+
Namespace: testNamespace,
480+
},
481+
Data: map[string][]byte{
482+
"tls.crt": []byte("cert-data"),
483+
"tls.key": []byte("key-data"),
484+
},
485+
}
486+
_, err := kubeclient.CoreV1().Secrets(testNamespace).Create(context.Background(), secret, metav1.CreateOptions{})
487+
require.NoError(t, err)
488+
489+
err = CreateCluster(context.Background(), kubeclient, testNamespace, "test-agent", testResourceProxyAddr, "missing-ca")
490+
491+
require.Error(t, err)
492+
require.Contains(t, err.Error(), "missing ca.crt")
493+
})
374494
}

principal/apis/auth/auth_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ func Test_Authenticate(t *testing.T) {
132132

133133
// Create manager with self cluster registration disabled
134134
kubeclient := kube.NewFakeKubeClient("argocd")
135-
mgr := clusterregistration.NewClusterRegistrationManager(false, "argocd", "resource-proxy:8443", kubeclient)
135+
mgr := clusterregistration.NewClusterRegistrationManager(false, "argocd", "resource-proxy:8443", "", kubeclient)
136136

137137
auths, err := NewServer(queues, ams, iss, WithClusterRegistrationManager(mgr))
138138
require.NoError(t, err)
@@ -155,7 +155,7 @@ func Test_Authenticate(t *testing.T) {
155155

156156
// Create manager with self cluster registration enabled but no CA secret to make it fail
157157
kubeclient := kube.NewFakeClientsetWithResources()
158-
mgr := clusterregistration.NewClusterRegistrationManager(true, "argocd", "resource-proxy:8443", kubeclient)
158+
mgr := clusterregistration.NewClusterRegistrationManager(true, "argocd", "resource-proxy:8443", "", kubeclient)
159159

160160
auths, err := NewServer(queues, ams, nil, WithClusterRegistrationManager(mgr))
161161
require.NoError(t, err)

principal/clusterregistration/clusterregistration.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,17 @@ type ClusterRegistrationManager struct {
2828
selfClusterRegistrationEnabled bool
2929
namespace string
3030
resourceProxyAddress string
31+
sharedClientCertSecretName string
3132
kubeclient kubernetes.Interface
3233
}
3334

34-
func NewClusterRegistrationManager(enabled bool, namespace, resourceProxyAddress string,
35+
func NewClusterRegistrationManager(enabled bool, namespace, resourceProxyAddress, sharedClientCertSecretName string,
3536
kubeclient kubernetes.Interface) *ClusterRegistrationManager {
3637
return &ClusterRegistrationManager{
3738
selfClusterRegistrationEnabled: enabled,
3839
namespace: namespace,
3940
resourceProxyAddress: resourceProxyAddress,
41+
sharedClientCertSecretName: sharedClientCertSecretName,
4042
kubeclient: kubeclient,
4143
}
4244
}
@@ -54,7 +56,7 @@ func (mgr *ClusterRegistrationManager) RegisterCluster(ctx context.Context, agen
5456
// Check if cluster secret already exists for the given agent
5557
exists, err := cluster.ClusterSecretExists(ctx, mgr.kubeclient, mgr.namespace, agentName)
5658
if err != nil {
57-
return fmt.Errorf("error while checking if cluster secret exists: %w", err)
59+
return fmt.Errorf("error while checking if cluster secret exists: %v", err)
5860
}
5961

6062
if exists {
@@ -65,8 +67,8 @@ func (mgr *ClusterRegistrationManager) RegisterCluster(ctx context.Context, agen
6567
// Create the cluster secret
6668
logCtx.Info("Registering agent's cluster")
6769

68-
if err := cluster.CreateCluster(ctx, mgr.kubeclient, mgr.namespace, agentName, mgr.resourceProxyAddress); err != nil {
69-
return fmt.Errorf("failed to self register agent's cluster and create cluster secret: %w", err)
70+
if err := cluster.CreateCluster(ctx, mgr.kubeclient, mgr.namespace, agentName, mgr.resourceProxyAddress, mgr.sharedClientCertSecretName); err != nil {
71+
return fmt.Errorf("failed to self register agent's cluster and create cluster secret: %v", err)
7072
}
7173

7274
logCtx.Info("Agent's self cluster registration completed successfully")

0 commit comments

Comments
 (0)