Skip to content

Commit 88f83bf

Browse files
author
rzisholz
committed
CP-21164: wire Conjur JWT config into the top-level CyberArk client
NewCyberArk and the agent config schema were still legacy-username/password-only at the top level, even though the underlying authenticator selection (previous PR) already supports Conjur JWT. Threads service_id/account/jwt_source/jwt_file_path through cyberark.service_id in the agent config down to NewCyberArk, so the config file is the single place an operator chooses which authentication method to use. config.go's call site and client_cyberark.go's signature change together — splitting them across two PRs would leave one non-building at every commit in between. jwt_source validation now delegates to the shared cyberark.ValidateJWTSource (previous PR) instead of re-implementing the same rule with different wording. Requiring an auth method (service_id or ARK_USERNAME) is now checked at config-validation time rather than left to cyberark.selectAuthenticator alone — that only runs at first upload, so a misconfigured agent could otherwise report healthy for up to a full config.period before failing. MachineHub's cluster_name fallback now checks cluster_id before ARK_USERNAME (cluster_id > ARK_USERNAME > empty) instead of dropping ARK_USERNAME as a fallback entirely — cluster_id still takes priority when both are set (needed since ARK_USERNAME doesn't exist on the Conjur JWT path), but an existing username/password install with neither cluster_name nor cluster_id configured keeps reporting under its ARK_USERNAME-derived name instead of an empty one. Adds FakeCyberArkUsernamePassword and matching integration tests: the only existing test exercising NewCyberArk's username/password path set ARK_USERNAME/ARK_SECRET but also passed a non-empty serviceID, which selectAuthenticator prioritises — so it silently tested Conjur regardless of those env vars, leaving the legacy path with no coverage through this seam. Also adds a two-upload regression test for the previous PR's cfg.Secret zeroing fix, which a single-upload test can't catch.
1 parent afbf35e commit 88f83bf

5 files changed

Lines changed: 355 additions & 46 deletions

File tree

pkg/agent/config.go

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"k8s.io/client-go/rest"
1818

1919
"github.com/jetstack/preflight/api"
20+
"github.com/jetstack/preflight/internal/cyberark"
2021
"github.com/jetstack/preflight/pkg/client"
2122
"github.com/jetstack/preflight/pkg/datagatherer"
2223
"github.com/jetstack/preflight/pkg/datagatherer/k8sdiscovery"
@@ -64,6 +65,9 @@ type Config struct {
6465
DataGatherers []DataGatherer `yaml:"data-gatherers"`
6566
VenafiCloud *VenafiCloudConfig `yaml:"venafi-cloud,omitempty"`
6667

68+
// CyberArk holds configuration for MachineHub mode (POC).
69+
CyberArk *CyberArkConfig `yaml:"cyberark,omitempty"`
70+
6771
// For testing purposes.
6872
InputPath string `yaml:"input-path"`
6973
// For testing purposes.
@@ -101,6 +105,19 @@ type VenafiCloudConfig struct {
101105
UploadPath string `yaml:"upload_path,omitempty"`
102106
}
103107

108+
// CyberArkConfig holds YAML configuration for MachineHub (CyberArk) mode (POC).
109+
type CyberArkConfig struct {
110+
// ServiceID is the authn-jwt service ID configured in Conjur (e.g. "dev-cluster").
111+
ServiceID string `yaml:"service_id"`
112+
// Account is the Conjur account name. Defaults to "conjur" when empty.
113+
Account string `yaml:"account"`
114+
// JWTSource selects how the agent obtains its JWT. Must be "" or "file" in the POC.
115+
JWTSource string `yaml:"jwt_source"`
116+
// JWTFilePath is the path to the JWT file when jwt_source is "file".
117+
// Defaults to the standard projected service-account token path when empty.
118+
JWTFilePath string `yaml:"jwt_file_path"`
119+
}
120+
104121
type AgentCmdFlags struct {
105122
// ConfigFilePath (--config-file, -c) is the path to the agent configuration
106123
// YAML file.
@@ -459,6 +476,9 @@ type CombinedConfig struct {
459476
TSGID string
460477
NGTSServerURL string
461478

479+
// MachineHub mode only.
480+
CyberArk CyberArkConfig
481+
462482
// Only used for testing purposes.
463483
OutputPath string
464484
InputPath string
@@ -753,6 +773,10 @@ func ValidateAndCombineConfig(log logr.Logger, cfg Config, flags AgentCmdFlags)
753773
clusterID = cfg.ClusterID
754774
case MachineHub:
755775
clusterName = cfg.ClusterName
776+
if clusterName == "" && cfg.ClusterID != "" {
777+
log.Info("Using cluster_id as cluster_name", "clusterID", cfg.ClusterID)
778+
clusterName = cfg.ClusterID
779+
}
756780
if clusterName == "" {
757781
if arkUsername, found := os.LookupEnv("ARK_USERNAME"); found {
758782
log.Info("Using ARK_USERNAME environment variable as cluster name", "clusterName", arkUsername)
@@ -762,8 +786,8 @@ func ValidateAndCombineConfig(log logr.Logger, cfg Config, flags AgentCmdFlags)
762786
if cfg.OrganizationID != "" {
763787
log.Info(fmt.Sprintf(`Ignoring the organization_id field in the config file. This field is not needed in %s mode.`, res.OutputMode))
764788
}
765-
if cfg.ClusterID != "" {
766-
log.Info(fmt.Sprintf(`Ignoring the cluster_id field in the config file. This field is not needed in %s mode.`, res.OutputMode))
789+
if clusterName == "" {
790+
log.Info("cluster_name is not set in MachineHub mode; cluster name will be empty")
767791
}
768792
}
769793
res.OrganizationID = organizationID
@@ -773,6 +797,27 @@ func ValidateAndCombineConfig(log logr.Logger, cfg Config, flags AgentCmdFlags)
773797
res.ClaimableCerts = cfg.ClaimableCerts
774798
}
775799

800+
// Validation of `cyberark.*` (MachineHub mode only).
801+
if res.OutputMode == MachineHub {
802+
ark := CyberArkConfig{}
803+
if cfg.CyberArk != nil {
804+
ark = *cfg.CyberArk
805+
}
806+
// service_id selects the Conjur JWT exchange. It is no longer required:
807+
// the agent also supports the legacy username/password method via
808+
// ARK_USERNAME/ARK_SECRET. Checked here, at config-validation time,
809+
// rather than left to cyberark.selectAuthenticator alone — that only
810+
// runs at first upload, so a misconfigured agent would otherwise
811+
// report healthy for up to a full config.period before failing.
812+
if ark.ServiceID == "" && os.Getenv("ARK_USERNAME") == "" {
813+
errs = multierror.Append(errs, fmt.Errorf("MachineHub mode requires either cyberark.service_id or ARK_USERNAME/ARK_SECRET"))
814+
}
815+
if err := cyberark.ValidateJWTSource(ark.JWTSource); err != nil {
816+
errs = multierror.Append(errs, fmt.Errorf("cyberark.jwt_source %w", err))
817+
}
818+
res.CyberArk = ark
819+
}
820+
776821
// Validation of `data-gatherers`.
777822
{
778823
if dgErr := ValidateDataGatherers(cfg.DataGatherers); dgErr != nil {
@@ -987,7 +1032,7 @@ func validateCredsAndCreateClient(log logr.Logger, flagCredentialsPath, flagClie
9871032
rootCAs *x509.CertPool
9881033
)
9891034
httpClient := http_client.NewDefaultClient(version.UserAgent(), rootCAs)
990-
outputClient, err = client.NewCyberArk(httpClient)
1035+
outputClient, err = client.NewCyberArk(httpClient, cfg.CyberArk.ServiceID, cfg.CyberArk.Account, cfg.CyberArk.JWTSource, cfg.CyberArk.JWTFilePath)
9911036
if err != nil {
9921037
errs = multierror.Append(errs, err)
9931038
}

pkg/agent/config_test.go

Lines changed: 196 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -658,56 +658,74 @@ func Test_ValidateAndCombineConfig(t *testing.T) {
658658
assert.Equal(t, VenafiConnection, got.OutputMode)
659659
})
660660

661-
const arkUsername = "cluster-1-region-1-cloud-1@cyberark.cloud.123456"
662-
663661
t.Run("--machine-hub selects MachineHub mode", func(t *testing.T) {
664662
t.Setenv("POD_NAMESPACE", "venafi")
665663
t.Setenv("KUBECONFIG", withFile(t, fakeKubeconfig))
666664
t.Setenv("ARK_SUBDOMAIN", "tlspk")
667-
t.Setenv("ARK_USERNAME", arkUsername)
668-
t.Setenv("ARK_SECRET", "test-secret")
669665
got, cl, err := ValidateAndCombineConfig(discardLogs(),
670-
withConfig(""),
666+
withConfig(testutil.Undent(`
667+
cluster_name: my-cluster
668+
cyberark:
669+
service_id: dev-cluster
670+
`)),
671671
withCmdLineFlags("--period", "1m", "--machine-hub"))
672672
require.NoError(t, err)
673673
assert.Equal(t, MachineHub, got.OutputMode)
674-
assert.Equal(t, arkUsername, got.ClusterName,
675-
"the ClusterName should default to the ARK_USERNAME value if the cluster_name in the config file is empty")
674+
assert.Equal(t, "my-cluster", got.ClusterName)
675+
assert.Equal(t, "dev-cluster", got.CyberArk.ServiceID)
676+
assert.IsType(t, &client.CyberArkClient{}, cl)
677+
})
678+
679+
t.Run("--machine-hub with cluster_id fallback when cluster_name is empty", func(t *testing.T) {
680+
t.Setenv("POD_NAMESPACE", "venafi")
681+
t.Setenv("KUBECONFIG", withFile(t, fakeKubeconfig))
682+
t.Setenv("ARK_SUBDOMAIN", "tlspk")
683+
got, cl, err := ValidateAndCombineConfig(discardLogs(),
684+
withConfig(testutil.Undent(`
685+
cluster_id: my-cluster-id
686+
cyberark:
687+
service_id: dev-cluster
688+
`)),
689+
withCmdLineFlags("--period", "1m", "--machine-hub"))
690+
require.NoError(t, err)
691+
assert.Equal(t, MachineHub, got.OutputMode)
692+
assert.Equal(t, "my-cluster-id", got.ClusterName,
693+
"cluster_id should be used as cluster_name when cluster_name is empty")
676694
assert.IsType(t, &client.CyberArkClient{}, cl)
677695
})
678696

679697
t.Run("--machine-hub with cluster_name override", func(t *testing.T) {
680698
t.Setenv("POD_NAMESPACE", "venafi")
681699
t.Setenv("KUBECONFIG", withFile(t, fakeKubeconfig))
682700
t.Setenv("ARK_SUBDOMAIN", "tlspk")
683-
t.Setenv("ARK_USERNAME", arkUsername)
684-
t.Setenv("ARK_SECRET", "test-secret")
685701
got, cl, err := ValidateAndCombineConfig(discardLogs(),
686702
withConfig(testutil.Undent(`
687703
cluster_name: override-cluster-name
688-
`)),
704+
cyberark:
705+
service_id: dev-cluster
706+
`)),
689707
withCmdLineFlags("--period", "1m", "--machine-hub"))
690708
require.NoError(t, err)
691709
assert.Equal(t, MachineHub, got.OutputMode)
692-
assert.Equal(t, "override-cluster-name", got.ClusterName,
693-
"the cluster_name in the config file should be used if not empty, even if ARK_USERNAME is set")
710+
assert.Equal(t, "override-cluster-name", got.ClusterName)
694711
assert.IsType(t, &client.CyberArkClient{}, cl)
695712
})
696713

697-
t.Run("--machine-hub without required environment variables", func(t *testing.T) {
714+
t.Run("--machine-hub without ARK_SUBDOMAIN environment variable", func(t *testing.T) {
698715
t.Setenv("POD_NAMESPACE", "venafi")
699716
t.Setenv("KUBECONFIG", withFile(t, fakeKubeconfig))
700717
t.Setenv("ARK_SUBDOMAIN", "")
701-
t.Setenv("ARK_USERNAME", "")
702-
t.Setenv("ARK_SECRET", "")
703718
got, cl, err := ValidateAndCombineConfig(discardLogs(),
704-
withConfig(""),
719+
withConfig(testutil.Undent(`
720+
cyberark:
721+
service_id: dev-cluster
722+
`)),
705723
withCmdLineFlags("--period", "1m", "--machine-hub"))
706724
assert.Equal(t, CombinedConfig{}, got)
707725
assert.Nil(t, cl)
708726
assert.EqualError(t, err, testutil.Undent(`
709727
validating creds: failed loading config using the MachineHub mode: 1 error occurred:
710-
* missing environment variables: ARK_SUBDOMAIN, ARK_USERNAME, ARK_SECRET
728+
* missing environment variables: ARK_SUBDOMAIN
711729
712730
`))
713731
})
@@ -1303,6 +1321,167 @@ func Test_ValidateAndCombineConfig_NGTS(t *testing.T) {
13031321
})
13041322
}
13051323

1324+
func TestConfig_CyberArk_Validation(t *testing.T) {
1325+
// Common env setup: ARK_SUBDOMAIN is the only required env var for MachineHub mode.
1326+
setEnv := func(t *testing.T) {
1327+
t.Helper()
1328+
t.Setenv("POD_NAMESPACE", "venafi")
1329+
t.Setenv("KUBECONFIG", withFile(t, fakeKubeconfig))
1330+
t.Setenv("ARK_SUBDOMAIN", "tlspk")
1331+
}
1332+
1333+
// service_id is not required at config-validation time as long as the
1334+
// legacy username/password method (ARK_USERNAME/ARK_SECRET, set via env,
1335+
// not config) is configured instead — one or the other is required,
1336+
// checked here rather than deferred to cyberark.selectAuthenticator at
1337+
// first upload. See the comment on this validation block in config.go.
1338+
t.Run("empty service_id is valid at config time when ARK_USERNAME is set", func(t *testing.T) {
1339+
setEnv(t)
1340+
t.Setenv("ARK_USERNAME", "test@example.com")
1341+
combined, _, err := ValidateAndCombineConfig(discardLogs(),
1342+
withConfig(testutil.Undent(`
1343+
cyberark:
1344+
service_id: ""
1345+
`)),
1346+
withCmdLineFlags("--period", "1m", "--machine-hub"))
1347+
require.NoError(t, err)
1348+
assert.Equal(t, "", combined.CyberArk.ServiceID)
1349+
})
1350+
1351+
t.Run("missing cyberark block is valid at config time when ARK_USERNAME is set", func(t *testing.T) {
1352+
setEnv(t)
1353+
t.Setenv("ARK_USERNAME", "test@example.com")
1354+
combined, _, err := ValidateAndCombineConfig(discardLogs(),
1355+
withConfig(""),
1356+
withCmdLineFlags("--period", "1m", "--machine-hub"))
1357+
require.NoError(t, err)
1358+
assert.Equal(t, "", combined.CyberArk.ServiceID)
1359+
})
1360+
1361+
t.Run("neither service_id nor ARK_USERNAME is an error at config time", func(t *testing.T) {
1362+
setEnv(t)
1363+
_, _, err := ValidateAndCombineConfig(discardLogs(),
1364+
withConfig(""),
1365+
withCmdLineFlags("--period", "1m", "--machine-hub"))
1366+
require.Error(t, err)
1367+
assert.Contains(t, err.Error(), "MachineHub mode requires either cyberark.service_id or ARK_USERNAME/ARK_SECRET")
1368+
})
1369+
1370+
// cluster_name fallback order: cluster_name > cluster_id > ARK_USERNAME > empty.
1371+
t.Run("cluster_name falls back to cluster_id when set, even if ARK_USERNAME is also set", func(t *testing.T) {
1372+
setEnv(t)
1373+
t.Setenv("ARK_USERNAME", "svc-agent@tenant")
1374+
got, _, err := ValidateAndCombineConfig(discardLogs(),
1375+
withConfig(testutil.Undent(`
1376+
cluster_id: my-cluster-id
1377+
cyberark:
1378+
service_id: dev-cluster
1379+
`)),
1380+
withCmdLineFlags("--period", "1m", "--machine-hub"))
1381+
require.NoError(t, err)
1382+
assert.Equal(t, "my-cluster-id", got.ClusterName)
1383+
})
1384+
1385+
t.Run("cluster_name falls back to ARK_USERNAME when cluster_id is unset", func(t *testing.T) {
1386+
setEnv(t)
1387+
t.Setenv("ARK_USERNAME", "svc-agent@tenant")
1388+
got, _, err := ValidateAndCombineConfig(discardLogs(),
1389+
withConfig(""),
1390+
withCmdLineFlags("--period", "1m", "--machine-hub"))
1391+
require.NoError(t, err)
1392+
assert.Equal(t, "svc-agent@tenant", got.ClusterName)
1393+
})
1394+
1395+
t.Run("cluster_name is empty when cluster_name, cluster_id, and ARK_USERNAME are all unset", func(t *testing.T) {
1396+
setEnv(t)
1397+
got, _, err := ValidateAndCombineConfig(discardLogs(),
1398+
withConfig(testutil.Undent(`
1399+
cyberark:
1400+
service_id: dev-cluster
1401+
`)),
1402+
withCmdLineFlags("--period", "1m", "--machine-hub"))
1403+
require.NoError(t, err)
1404+
assert.Equal(t, "", got.ClusterName)
1405+
})
1406+
1407+
t.Run("jwt_source spiffe is rejected", func(t *testing.T) {
1408+
setEnv(t)
1409+
_, _, err := ValidateAndCombineConfig(discardLogs(),
1410+
withConfig(testutil.Undent(`
1411+
cyberark:
1412+
service_id: dev-cluster
1413+
jwt_source: spiffe
1414+
`)),
1415+
withCmdLineFlags("--period", "1m", "--machine-hub"))
1416+
require.Error(t, err)
1417+
assert.Contains(t, err.Error(), `cyberark.jwt_source "spiffe" is not supported`)
1418+
})
1419+
1420+
t.Run("jwt_source file is accepted", func(t *testing.T) {
1421+
setEnv(t)
1422+
got, cl, err := ValidateAndCombineConfig(discardLogs(),
1423+
withConfig(testutil.Undent(`
1424+
cyberark:
1425+
service_id: dev-cluster
1426+
jwt_source: file
1427+
jwt_file_path: /var/run/secrets/tokens/agent-token
1428+
`)),
1429+
withCmdLineFlags("--period", "1m", "--machine-hub"))
1430+
require.NoError(t, err)
1431+
assert.Equal(t, "dev-cluster", got.CyberArk.ServiceID)
1432+
assert.Equal(t, "file", got.CyberArk.JWTSource)
1433+
assert.Equal(t, "/var/run/secrets/tokens/agent-token", got.CyberArk.JWTFilePath)
1434+
assert.IsType(t, &client.CyberArkClient{}, cl)
1435+
})
1436+
1437+
t.Run("jwt_source empty string is accepted", func(t *testing.T) {
1438+
setEnv(t)
1439+
got, cl, err := ValidateAndCombineConfig(discardLogs(),
1440+
withConfig(testutil.Undent(`
1441+
cyberark:
1442+
service_id: dev-cluster
1443+
`)),
1444+
withCmdLineFlags("--period", "1m", "--machine-hub"))
1445+
require.NoError(t, err)
1446+
assert.Equal(t, "dev-cluster", got.CyberArk.ServiceID)
1447+
assert.Equal(t, "", got.CyberArk.JWTSource)
1448+
assert.IsType(t, &client.CyberArkClient{}, cl)
1449+
})
1450+
1451+
t.Run("account and jwt_file_path are optional", func(t *testing.T) {
1452+
setEnv(t)
1453+
got, _, err := ValidateAndCombineConfig(discardLogs(),
1454+
withConfig(testutil.Undent(`
1455+
cyberark:
1456+
service_id: dev-cluster
1457+
account: myaccount
1458+
jwt_file_path: /tmp/token
1459+
`)),
1460+
withCmdLineFlags("--period", "1m", "--machine-hub"))
1461+
require.NoError(t, err)
1462+
assert.Equal(t, "myaccount", got.CyberArk.Account)
1463+
assert.Equal(t, "/tmp/token", got.CyberArk.JWTFilePath)
1464+
})
1465+
1466+
t.Run("cyberark block is ignored in non-MachineHub modes", func(t *testing.T) {
1467+
t.Setenv("POD_NAMESPACE", "venafi")
1468+
fakeCredsPath := withFile(t, `{"user_id":"foo","user_secret":"bar","client_id": "baz","client_secret": "foobar","auth_server_domain":"bazbar"}`)
1469+
got, _, err := ValidateAndCombineConfig(discardLogs(),
1470+
withConfig(testutil.Undent(`
1471+
server: https://preflight.jetstack.io
1472+
organization_id: my-org
1473+
cluster_id: my-cluster
1474+
period: 1h
1475+
cyberark:
1476+
service_id: should-be-ignored
1477+
`)),
1478+
withCmdLineFlags("--credentials-file", fakeCredsPath))
1479+
require.NoError(t, err)
1480+
// CyberArk config is not copied into CombinedConfig for non-MachineHub modes.
1481+
assert.Equal(t, CyberArkConfig{}, got.CyberArk)
1482+
})
1483+
}
1484+
13061485
const fakePrivKeyPEM = `-----BEGIN PRIVATE KEY-----
13071486
MHcCAQEEIFptpPXOvEWDrYkiMhyEH1+FB1GwtwX2tyXH4KtBO6g7oAoGCCqGSM49
13081487
AwEHoUQDQgAE/BsIwagYc4YUjSSFyqcStj2qliAkdVGlMoJbMuXupzQ9Qs4TX5Pl

0 commit comments

Comments
 (0)