Skip to content

Commit 3884171

Browse files
committed
feat: use JWT bearer token for self agent registration
Assisted by: Cursor Signed-off-by: Jayendra Parsai <jparsai@redhat.com>
1 parent a247abd commit 3884171

File tree

16 files changed

+1224
-311
lines changed

16 files changed

+1224
-311
lines changed

cmd/argocd-agent/principal.go

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

102-
enableSelfClusterRegistration bool
103-
selfClusterRegistrationSharedCert string
102+
enableSelfClusterRegistration bool
103+
selfRegClientCertSecretName string
104104
)
105105
command := &cobra.Command{
106106
Use: "principal",
@@ -329,10 +329,18 @@ func NewPrincipalRunCommand() *cobra.Command {
329329
opts = append(opts, principal.WithRedis(redisAddress, redisPassword, redisCompressionType))
330330
opts = append(opts, principal.WithHealthzPort(healthzPort))
331331

332-
// Self cluster registration options
332+
// Self cluster registration validation and options
333+
if enableSelfClusterRegistration {
334+
if selfRegClientCertSecretName == "" {
335+
cmdutil.Fatal("Self cluster registration requires --self-reg-client-cert-secret to be set")
336+
}
337+
if !enableResourceProxy {
338+
cmdutil.Fatal("Self cluster registration requires --enable-resource-proxy to be enabled")
339+
}
340+
}
333341
opts = append(opts, principal.WithClusterRegistration(enableSelfClusterRegistration))
334-
if selfClusterRegistrationSharedCert != "" {
335-
opts = append(opts, principal.WithSelfClusterRegistrationSharedCert(selfClusterRegistrationSharedCert))
342+
if selfRegClientCertSecretName != "" {
343+
opts = append(opts, principal.WithClientCertSecretName(selfRegClientCertSecretName))
336344
}
337345

338346
s, err := principal.NewServer(ctx, kubeConfig, namespace, opts...)
@@ -493,11 +501,10 @@ func NewPrincipalRunCommand() *cobra.Command {
493501

494502
command.Flags().BoolVar(&enableSelfClusterRegistration, "enable-self-cluster-registration",
495503
env.BoolWithDefault("ARGOCD_PRINCIPAL_ENABLE_SELF_CLUSTER_REGISTRATION", false),
496-
"Allow agents with valid client certificates to self-register on connection")
497-
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)")
504+
"Allow agents with valid credentials to self-register on connection (requires --self-reg-client-cert-secret)")
505+
command.Flags().StringVar(&selfRegClientCertSecretName, "self-reg-client-cert-secret",
506+
env.StringWithDefault("ARGOCD_PRINCIPAL_SELF_REG_CLIENT_CERT_SECRET", nil, ""),
507+
"TLS secret containing shared client cert for self-registered cluster secrets (must have tls.crt, tls.key, ca.crt)")
501508

502509
return command
503510
}

hack/dev-env/start-principal.sh

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,14 @@ if [ -f "$E2E_ENV_FILE" ]; then
4545
source "$E2E_ENV_FILE"
4646
export ARGOCD_PRINCIPAL_ENABLE_WEBSOCKET=${ARGOCD_PRINCIPAL_ENABLE_WEBSOCKET:-false}
4747
export ARGOCD_PRINCIPAL_ENABLE_SELF_CLUSTER_REGISTRATION=${ARGOCD_PRINCIPAL_ENABLE_SELF_CLUSTER_REGISTRATION:-false}
48+
if [ -n "$ARGOCD_PRINCIPAL_SELF_REG_CLIENT_CERT_SECRET" ]; then
49+
export ARGOCD_PRINCIPAL_SELF_REG_CLIENT_CERT_SECRET
50+
fi
4851
fi
4952

50-
export ARGOCD_AGENT_RESOURCE_PROXY=$(ip r show default | sed -e 's,.*\ src\ ,,' | sed -e 's,\ metric.*$,,')
53+
ARGOCD_AGENT_RESOURCE_PROXY=$(ip r show default | sed -e 's,.*\ src\ ,,' | sed -e 's,\ metric.*$,,')
54+
export ARGOCD_AGENT_RESOURCE_PROXY
5155

52-
SCRIPTPATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )"
5356
go run github.com/argoproj-labs/argocd-agent/cmd/argocd-agent principal \
5457
--allowed-namespaces '*' \
5558
--kubecontext vcluster-control-plane \

internal/argocd/cluster/cluster.go

Lines changed: 114 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,19 @@ package cluster
1616

1717
import (
1818
"context"
19-
"crypto/x509"
2019
"errors"
2120
"fmt"
2221
"time"
2322

24-
"github.com/argoproj-labs/argocd-agent/internal/config"
2523
"github.com/argoproj-labs/argocd-agent/internal/event"
26-
"github.com/argoproj-labs/argocd-agent/internal/tlsutil"
2724
"github.com/redis/go-redis/v9"
2825
"github.com/redis/go-redis/v9/maintnotifications"
2926
"github.com/sirupsen/logrus"
3027

3128
appv1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
3229
cacheutil "github.com/argoproj/argo-cd/v3/util/cache"
3330
appstatecache "github.com/argoproj/argo-cd/v3/util/cache/appstate"
31+
"github.com/argoproj/argo-cd/v3/util/db"
3432
v1 "k8s.io/api/core/v1"
3533
apierrors "k8s.io/apimachinery/pkg/api/errors"
3634
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -191,36 +189,35 @@ func NewClusterCacheInstance(redisAddress, redisPassword string, redisCompressio
191189
return clusterCache, nil
192190
}
193191

194-
// CreateCluster creates a cluster secret for an agent's cluster on the principal.
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")
192+
// TokenIssuer is an interface for issuing resource proxy tokens.
193+
// This is a subset of the issuer.Issuer interface to avoid circular dependencies.
194+
type TokenIssuer interface {
195+
IssueResourceProxyToken(agentName string) (string, error)
196+
}
216197

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-
}
198+
// CreateClusterWithBearerToken creates a cluster secret for an agent using both mTLS and JWT bearer token authentication.
199+
// - The shared client certificate (mTLS) proves the request comes from a trusted ArgoCD server
200+
// - The JWT bearer token identifies which specific agent is being accessed
201+
func CreateClusterWithBearerToken(ctx context.Context, kubeclient kubernetes.Interface, namespace, agentName, resourceProxyAddress string, tokenIssuer TokenIssuer, clientCertSecretName string) error {
202+
logCtx := log().WithField("agent", agentName).WithField("process", "self-registration")
203+
logCtx.Info("Creating self-registered cluster secret with shared client cert and bearer token")
204+
205+
// Generate bearer token for this agent
206+
bearerToken, err := tokenIssuer.IssueResourceProxyToken(agentName)
207+
if err != nil {
208+
logCtx.WithError(err).Error("Failed to issue resource proxy token")
209+
return fmt.Errorf("could not issue resource proxy token: %v", err)
210+
}
211+
logCtx.Info("Successfully issued resource proxy token")
212+
213+
// Read shared client certificate for mTLS
214+
clientCert, clientKey, caData, err := readClientCertFromSecret(ctx, kubeclient, namespace, clientCertSecretName)
215+
if err != nil {
216+
logCtx.WithError(err).Error("Failed to read client certificate from secret")
217+
return fmt.Errorf("could not read client certificate from secret %s: %v", clientCertSecretName, err)
221218
}
219+
logCtx.Info("Successfully read client certificate from secret")
222220

223-
// Note: this structure has to be same as manual creation done by `argocd-agentctl agent create <agent_name>`
224221
cluster := &appv1.Cluster{
225222
Server: fmt.Sprintf("https://%s?agentName=%s", resourceProxyAddress, agentName),
226223
Name: agentName,
@@ -229,6 +226,7 @@ func CreateCluster(ctx context.Context, kubeclient kubernetes.Interface, namespa
229226
LabelKeySelfRegisteredCluster: "true",
230227
},
231228
Config: appv1.ClusterConfig{
229+
BearerToken: bearerToken,
232230
TLSClientConfig: appv1.TLSClientConfig{
233231
CertData: []byte(clientCert),
234232
KeyData: []byte(clientKey),
@@ -237,97 +235,140 @@ func CreateCluster(ctx context.Context, kubeclient kubernetes.Interface, namespa
237235
},
238236
}
239237

240-
// Convert cluster object to Kubernetes secret object
241238
secret := &v1.Secret{
242239
ObjectMeta: metav1.ObjectMeta{
243240
Name: getClusterSecretName(agentName),
244241
Namespace: namespace,
245242
},
246243
}
247244
if err := ClusterToSecret(cluster, secret); err != nil {
245+
logCtx.WithError(err).Error("Failed to convert cluster to secret")
248246
return fmt.Errorf("could not convert cluster to secret: %v", err)
249247
}
250248

251-
// Create the secret to register the agent's cluster.
252249
if _, err = kubeclient.CoreV1().Secrets(namespace).Create(ctx, secret, metav1.CreateOptions{}); err != nil {
253250
if apierrors.IsAlreadyExists(err) {
251+
logCtx.Info("Cluster secret already exists, skipping creation")
254252
return nil
255253
}
256-
return fmt.Errorf("could not create cluster secret to register agent's cluster: %v", err)
254+
logCtx.WithError(err).Error("Failed to create cluster secret")
255+
return fmt.Errorf("could not create cluster secret: %w", err)
256+
}
257+
258+
logCtx.Info("Successfully created cluster secret with shared client cert and bearer token")
259+
return nil
260+
}
261+
262+
// IsClusterSelfRegistered checks if a cluster secret was created by self-registration.
263+
func IsClusterSelfRegistered(ctx context.Context, kubeclient kubernetes.Interface, namespace, agentName string) (bool, error) {
264+
secret, err := kubeclient.CoreV1().Secrets(namespace).Get(ctx, getClusterSecretName(agentName), metav1.GetOptions{})
265+
if err != nil {
266+
return false, err
267+
}
268+
return secret.Labels[LabelKeySelfRegisteredCluster] == "true", nil
269+
}
270+
271+
// UpdateClusterBearerToken updates an existing cluster secret with a new bearer token.
272+
// This is used when the signing key rotates and existing tokens become invalid.
273+
func UpdateClusterBearerToken(ctx context.Context, kubeclient kubernetes.Interface, namespace, agentName string, tokenIssuer TokenIssuer) error {
274+
logCtx := log().WithField("agent", agentName).WithField("process", "self-registration")
275+
logCtx.Info("Updating cluster secret bearer token")
276+
277+
secretName := getClusterSecretName(agentName)
278+
279+
secret, err := kubeclient.CoreV1().Secrets(namespace).Get(ctx, secretName, metav1.GetOptions{})
280+
if err != nil {
281+
logCtx.WithError(err).Error("Failed to get cluster secret")
282+
return fmt.Errorf("could not get cluster secret: %v", err)
283+
}
284+
285+
cluster, err := db.SecretToCluster(secret)
286+
if err != nil {
287+
logCtx.WithError(err).Error("Failed to parse cluster secret")
288+
return fmt.Errorf("could not parse cluster secret: %v", err)
289+
}
290+
291+
// Generate new bearer token
292+
bearerToken, err := tokenIssuer.IssueResourceProxyToken(agentName)
293+
if err != nil {
294+
logCtx.WithError(err).Error("Failed to issue new resource proxy token")
295+
return fmt.Errorf("could not issue resource proxy token: %v", err)
296+
}
297+
logCtx.Info("Successfully issued new resource proxy token")
298+
299+
cluster.Config.BearerToken = bearerToken
300+
301+
if err := ClusterToSecret(cluster, secret); err != nil {
302+
logCtx.WithError(err).Error("Failed to convert cluster to secret")
303+
return fmt.Errorf("could not convert cluster to secret: %v", err)
304+
}
305+
306+
if _, err = kubeclient.CoreV1().Secrets(namespace).Update(ctx, secret, metav1.UpdateOptions{}); err != nil {
307+
logCtx.WithError(err).Error("Failed to update cluster secret")
308+
return fmt.Errorf("could not update cluster secret: %v", err)
257309
}
258310

311+
logCtx.Info("Successfully updated cluster secret bearer token")
259312
return nil
260313
}
261314

262-
// readSharedClientCertFromSecret reads TLS credentials from an existing Kubernetes TLS secret.
315+
// readClientCertFromSecret reads TLS credentials from an existing Kubernetes TLS secret.
263316
// 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{})
317+
func readClientCertFromSecret(ctx context.Context, kubeclient kubernetes.Interface, namespace, secretName string) (clientCert, clientKey, caData string, err error) {
318+
secret, err := kubeclient.CoreV1().Secrets(namespace).Get(ctx, secretName, metav1.GetOptions{})
266319
if err != nil {
267-
return "", "", "", fmt.Errorf("could not read TLS secret %s/%s: %v", namespace, sharedClientCertSecretName, err)
320+
return "", "", "", fmt.Errorf("could not read TLS secret %s/%s: %w", namespace, secretName, err)
268321
}
269322

270323
certData, ok := secret.Data["tls.crt"]
271324
if !ok {
272-
return "", "", "", fmt.Errorf("secret %s/%s missing tls.crt", namespace, sharedClientCertSecretName)
325+
return "", "", "", fmt.Errorf("secret %s/%s missing tls.crt", namespace, secretName)
273326
}
274327

275328
keyData, ok := secret.Data["tls.key"]
276329
if !ok {
277-
return "", "", "", fmt.Errorf("secret %s/%s missing tls.key", namespace, sharedClientCertSecretName)
330+
return "", "", "", fmt.Errorf("secret %s/%s missing tls.key", namespace, secretName)
278331
}
279332

280333
// CA cert can be in ca.crt (cert-manager style) or tls.crt of a separate CA secret
281334
caBytes, ok := secret.Data["ca.crt"]
282335
if !ok {
283-
return "", "", "", fmt.Errorf("secret %s/%s missing ca.crt", namespace, sharedClientCertSecretName)
336+
return "", "", "", fmt.Errorf("secret %s/%s missing ca.crt", namespace, secretName)
284337
}
285338

286339
return string(certData), string(keyData), string(caBytes), nil
287340
}
288341

289-
// ClusterSecretExists checks if a cluster secret exists for the given agent.
290-
func ClusterSecretExists(ctx context.Context, kubeclient kubernetes.Interface, namespace, agentName string) (bool, error) {
291-
if _, err := kubeclient.CoreV1().Secrets(namespace).Get(ctx, getClusterSecretName(agentName), metav1.GetOptions{}); err != nil {
292-
if apierrors.IsNotFound(err) {
293-
return false, nil
294-
}
295-
return false, err
296-
}
297-
return true, nil
298-
}
299-
300-
func generateAgentClientCert(ctx context.Context, kubeclient kubernetes.Interface, namespace, agentName, caSecretName string) (clientCert, clientKey, caData string, err error) {
342+
// GetClusterBearerToken retrieves the bearer token from an existing cluster secret.
343+
func GetClusterBearerToken(ctx context.Context, kubeclient kubernetes.Interface, namespace, agentName string) (string, error) {
344+
logCtx := log().WithField("agent", agentName).WithField("process", "self-registration")
345+
logCtx.Info("Retrieving bearer token from cluster secret")
301346

302-
// Read the CA certificate from the principal's CA secret
303-
tlsCert, err := tlsutil.TLSCertFromSecret(ctx, kubeclient, namespace, caSecretName)
347+
secretName := getClusterSecretName(agentName)
348+
secret, err := kubeclient.CoreV1().Secrets(namespace).Get(ctx, secretName, metav1.GetOptions{})
304349
if err != nil {
305-
err = fmt.Errorf("could not read CA secret: %v", err)
306-
return
350+
logCtx.WithError(err).Info("Failed to get cluster secret")
351+
return "", fmt.Errorf("could not get cluster secret: %v", err)
307352
}
308353

309-
// Parse CA certificate from PEM format
310-
signerCert, err := x509.ParseCertificate(tlsCert.Certificate[0])
354+
cluster, err := db.SecretToCluster(secret)
311355
if err != nil {
312-
err = fmt.Errorf("could not parse CA certificate: %v", err)
313-
return
356+
logCtx.WithError(err).Error("Failed to parse cluster secret")
357+
return "", fmt.Errorf("could not parse cluster secret: %v", err)
314358
}
315359

316-
// Generate a client cert with agent name as CN and sign it with the CA's cert and key
317-
clientCert, clientKey, err = tlsutil.GenerateClientCertificate(agentName, signerCert, tlsCert.PrivateKey)
318-
if err != nil {
319-
err = fmt.Errorf("could not create client cert: %v", err)
320-
return
321-
}
360+
return cluster.Config.BearerToken, nil
361+
}
322362

323-
// Convert CA certificate to PEM format
324-
caData, err = tlsutil.CertDataToPEM(tlsCert.Certificate[0])
325-
if err != nil {
326-
err = fmt.Errorf("could not convert CA certificate to PEM format: %v", err)
327-
return
363+
// ClusterSecretExists checks if a cluster secret exists for the given agent.
364+
func ClusterSecretExists(ctx context.Context, kubeclient kubernetes.Interface, namespace, agentName string) (bool, error) {
365+
if _, err := kubeclient.CoreV1().Secrets(namespace).Get(ctx, getClusterSecretName(agentName), metav1.GetOptions{}); err != nil {
366+
if apierrors.IsNotFound(err) {
367+
return false, nil
368+
}
369+
return false, err
328370
}
329-
330-
return
371+
return true, nil
331372
}
332373

333374
func getClusterSecretName(agentName string) string {

0 commit comments

Comments
 (0)