Skip to content

VC-41203: Allow users to select the Machine Hub mode #653

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
169 changes: 126 additions & 43 deletions pkg/agent/config.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package agent

import (
"errors"
"fmt"
"io"
"net/url"
Expand All @@ -10,7 +11,6 @@ import (

"github.com/go-logr/logr"
"github.com/hashicorp/go-multierror"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
"k8s.io/client-go/rest"
Expand Down Expand Up @@ -39,7 +39,7 @@ type Config struct {
// https://preflight.jetstack.io in Jetstack Secure OAuth and Jetstack
// Secure API Token modes, and https://api.venafi.cloud in Venafi Cloud Key
// Pair Service Account mode. It is ignored in Venafi Cloud VenafiConnection
// mode.
// mode and in MachineHub mode.
Server string `yaml:"server"`

// OrganizationID is only used in Jetstack Secure OAuth and Jetstack Secure
Expand All @@ -62,6 +62,9 @@ type Config struct {
ExcludeAnnotationKeysRegex []string `yaml:"exclude-annotation-keys-regex"`
// Skips label keys that match the given set of regular expressions.
ExcludeLabelKeysRegex []string `yaml:"exclude-label-keys-regex"`

// MachineHub holds config specific to MachineHub mode.
MachineHub MachineHubConfig `yaml:"machineHub"`
}

type Endpoint struct {
Expand Down Expand Up @@ -89,6 +92,33 @@ type VenafiCloudConfig struct {
UploadPath string `yaml:"upload_path,omitempty"`
}

// MachineHubConfig holds configuration values specific to the CyberArk Machine Hub integration
type MachineHubConfig struct {
// Subdomain is the subdomain indicating where data should be pushed. Used
// for querying the Service Discovery Service to discover the Identity API
// URL.
Subdomain string `yaml:"subdomain"`

// CredentialsSecretName is the name of a Kubernetes Secret in the same
// namespace as the agent, which will be watched for a username and password
// to send to CyberArk Identity for authentication.
CredentialsSecretName string `yaml:"credentialsSecretName"`
Comment on lines +102 to +105
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: do you think we could include the secret watcher in this PR, or should that be left for a separate PR? It's definitely something we'll need!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll add the watcher to the next PR 😅

}

func (mhc MachineHubConfig) Validate() error {
var errs []error

if mhc.Subdomain == "" {
errs = append(errs, fmt.Errorf("subdomain must not be empty in MachineHub mode"))
}

if mhc.CredentialsSecretName == "" {
errs = append(errs, fmt.Errorf("credentialsSecretName must not be empty in MachineHub mode"))
}

return errors.Join(errs...)
}

type AgentCmdFlags struct {
// ConfigFilePath (--config-file, -c) is the path to the agent configuration
// YAML file.
Expand All @@ -103,6 +133,9 @@ type AgentCmdFlags struct {
// --credentials-file.
VenafiCloudMode bool

// MachineHubMode configures the agent to send data to CyberArk Machine Hub.
MachineHubMode bool

// ClientID (--client-id) is the clientID in case of Venafi Cloud Key Pair
// Service Account mode.
ClientID string
Expand Down Expand Up @@ -303,30 +336,48 @@ func InitAgentCmdFlags(c *cobra.Command, cfg *AgentCmdFlags) {
)
c.PersistentFlags().MarkDeprecated("disable-compression", "no longer has an effect")

// This is a hidden feature flag we use to build the "Machine Hub" feature
// gradually without impacting customers. Once the feature is GA, we will
// turn this flag "on" by default.
c.PersistentFlags().BoolVar(
&cfg.MachineHubMode,
"machine-hub",
false,
"Enables the MachineHub mode. The agent will push data to CyberArk MachineHub.",
)
c.PersistentFlags().MarkHidden("machine-hub")

}

type AuthMode string
// TLSPKMode controls how to authenticate to TLSPK / Jetstack Secure. Only one
// TLSPKMode may be provided if using using those backends.
type TLSPKMode string

const (
JetstackSecureOAuth AuthMode = "Jetstack Secure OAuth"
JetstackSecureAPIToken AuthMode = "Jetstack Secure API Token"
VenafiCloudKeypair AuthMode = "Venafi Cloud Key Pair Service Account"
VenafiCloudVenafiConnection AuthMode = "Venafi Cloud VenafiConnection"
JetstackSecureOAuth TLSPKMode = "Jetstack Secure OAuth"
JetstackSecureAPIToken TLSPKMode = "Jetstack Secure API Token"
VenafiCloudKeypair TLSPKMode = "Venafi Cloud Key Pair Service Account"
VenafiCloudVenafiConnection TLSPKMode = "Venafi Cloud VenafiConnection"

// It is possible to push to both MachineHub and TLSPK. With this mode, the
// agent will only push to MachineHub and not to TLSPK.
Off TLSPKMode = "MachineHub only"
)

// The command-line flags and the config file are combined into this struct by
// ValidateAndCombineConfig.
type CombinedConfig struct {
AuthMode AuthMode

// Used by all modes.
ClusterID string
DataGatherers []DataGatherer
Period time.Duration
BackoffMaxTime time.Duration
InstallNS string
StrictMode bool
OneShot bool
InstallNS string

TLSPKMode TLSPKMode

// Used by all TLSPK modes.
ClusterID string

// Used by JetstackSecureOAuth, JetstackSecureAPIToken, and
// VenafiCloudKeypair. Ignored in VenafiCloudVenafiConnection mode.
Expand All @@ -351,6 +402,11 @@ type CombinedConfig struct {
// Only used for testing purposes.
OutputPath string
InputPath string

// MachineHub-related settings.
MachineHubMode bool
MachineHubSubdomain string
MachineHubCredentialsSecretName string
}

// ValidateAndCombineConfig combines and validates the input configuration with
Expand All @@ -366,9 +422,23 @@ func ValidateAndCombineConfig(log logr.Logger, cfg Config, flags AgentCmdFlags)
res := CombinedConfig{}
var errs error

if flags.MachineHubMode {
err := cfg.MachineHub.Validate()
if err != nil {
return CombinedConfig{}, nil, fmt.Errorf("invalid MachineHub config provided: %w", err)
}

res.MachineHubMode = true
res.MachineHubSubdomain = cfg.MachineHub.Subdomain
res.MachineHubCredentialsSecretName = cfg.MachineHub.CredentialsSecretName

keysAndValues := []any{"credentialsSecretName", res.MachineHubCredentialsSecretName}
log.V(logs.Info).Info("Will push to CyberArk MachineHub using a username and password loaded from a Kubernetes Secret", keysAndValues...)
}

{
var (
mode AuthMode
mode TLSPKMode
reason string
keysAndValues []any
)
Expand Down Expand Up @@ -396,19 +466,30 @@ func ValidateAndCombineConfig(log logr.Logger, cfg Config, flags AgentCmdFlags)
mode = JetstackSecureOAuth
reason = "--credentials-file was specified without --venafi-cloud"
default:
return CombinedConfig{}, nil, fmt.Errorf("no auth mode specified. You can use one of four auth modes:\n" +
" - Use (--venafi-cloud with --credentials-file) or (--client-id with --private-key-path) to use the " + string(VenafiCloudKeypair) + " mode.\n" +
" - Use --venafi-connection for the " + string(VenafiCloudVenafiConnection) + " mode.\n" +
" - Use --credentials-file alone if you want to use the " + string(JetstackSecureOAuth) + " mode.\n" +
" - Use --api-token if you want to use the " + string(JetstackSecureAPIToken) + " mode.\n")
if !flags.MachineHubMode {
return CombinedConfig{}, nil, fmt.Errorf("no TLSPK mode specified and MachineHub mode is disabled. You must either enable the MachineHub mode (using --machine-hub), or enable one of the TLSPK modes.\n" +
"To enable one of the TLSPK modes, you can:\n" +
" - Use (--venafi-cloud with --credentials-file) or (--client-id with --private-key-path) to use the " + string(VenafiCloudKeypair) + " mode.\n" +
" - Use --venafi-connection for the " + string(VenafiCloudVenafiConnection) + " mode.\n" +
" - Use --credentials-file alone if you want to use the " + string(JetstackSecureOAuth) + " mode.\n" +
" - Use --api-token if you want to use the " + string(JetstackSecureAPIToken) + " mode.\n" +
"Note that it is possible to use one of the TLSPK modes along with the MachineHub mode (--machine-hub).")
}

mode = Off
}
res.AuthMode = mode

keysAndValues = append(keysAndValues, "mode", mode, "reason", reason)
log.V(logs.Debug).Info("Authentication mode", keysAndValues...)
if mode != Off {
log.V(logs.Debug).Info("Configured to push to Venafi", keysAndValues...)
}

res.TLSPKMode = mode
}

// Validation and defaulting of `server` and the deprecated `endpoint.path`.
{
if res.TLSPKMode != Off {
// Only relevant if using TLSPK backends
hasEndpointField := cfg.Endpoint.Host != "" && cfg.Endpoint.Path != ""
hasServerField := cfg.Server != ""
var server string
Expand All @@ -430,7 +511,7 @@ func ValidateAndCombineConfig(log logr.Logger, cfg Config, flags AgentCmdFlags)
endpointPath = cfg.Endpoint.Path
case !hasServerField && !hasEndpointField:
server = "https://preflight.jetstack.io"
if res.AuthMode == VenafiCloudKeypair {
if res.TLSPKMode == VenafiCloudKeypair {
// The VenafiCloudVenafiConnection mode doesn't need a server.
server = client.VenafiCloudProdURL
}
Expand All @@ -439,7 +520,7 @@ func ValidateAndCombineConfig(log logr.Logger, cfg Config, flags AgentCmdFlags)
if urlErr != nil || url.Hostname() == "" {
errs = multierror.Append(errs, fmt.Errorf("server %q is not a valid URL", server))
}
if res.AuthMode == VenafiCloudVenafiConnection && server != "" {
if res.TLSPKMode == VenafiCloudVenafiConnection && server != "" {
log.Info(fmt.Sprintf("ignoring the server field specified in the config file. In %s mode, this field is not needed.", VenafiCloudVenafiConnection))
server = ""
}
Expand All @@ -451,9 +532,9 @@ func ValidateAndCombineConfig(log logr.Logger, cfg Config, flags AgentCmdFlags)
{
var uploadPath string
switch {
case res.AuthMode == VenafiCloudKeypair:
case res.TLSPKMode == VenafiCloudKeypair:
if cfg.VenafiCloud == nil || cfg.VenafiCloud.UploadPath == "" {
errs = multierror.Append(errs, fmt.Errorf("the venafi-cloud.upload_path field is required when using the %s mode", res.AuthMode))
errs = multierror.Append(errs, fmt.Errorf("the venafi-cloud.upload_path field is required when using the %s mode", res.TLSPKMode))
break // Skip to the end of the switch statement.
}
_, urlErr := url.Parse(cfg.VenafiCloud.UploadPath)
Expand All @@ -463,14 +544,14 @@ func ValidateAndCombineConfig(log logr.Logger, cfg Config, flags AgentCmdFlags)
}

uploadPath = cfg.VenafiCloud.UploadPath
case res.AuthMode == VenafiCloudVenafiConnection:
case res.TLSPKMode == VenafiCloudVenafiConnection:
// The venafi-cloud.upload_path was initially meant to let users
// configure HTTP proxies, but it has never been used since HTTP
// proxies don't rewrite paths. Thus, we've disabled the ability to
// change this value with the new --venafi-connection flag, and this
// field is simply ignored.
if cfg.VenafiCloud != nil && cfg.VenafiCloud.UploadPath != "" {
log.Info(fmt.Sprintf(`ignoring the venafi-cloud.upload_path field in the config file. In %s mode, this field is not needed.`, res.AuthMode))
log.Info(fmt.Sprintf(`ignoring the venafi-cloud.upload_path field in the config file. In %s mode, this field is not needed.`, res.TLSPKMode))
}
uploadPath = ""
}
Expand All @@ -488,26 +569,26 @@ func ValidateAndCombineConfig(log logr.Logger, cfg Config, flags AgentCmdFlags)
// https://venafi.atlassian.net/browse/VC-35385 is done.
{
if cfg.VenafiCloud != nil && cfg.VenafiCloud.UploaderID != "" {
log.Info(fmt.Sprintf(`ignoring the venafi-cloud.uploader_id field in the config file. This field is not needed in %s mode.`, res.AuthMode))
log.Info(fmt.Sprintf(`ignoring the venafi-cloud.uploader_id field in the config file. This field is not needed in %s mode.`, res.TLSPKMode))
}
}

// Validation of `cluster_id` and `organization_id`.
{
if res.TLSPKMode != Off {
var clusterID string
var organizationID string // Only used by the old jetstack-secure mode.
switch {
case res.AuthMode == VenafiCloudKeypair:
case res.TLSPKMode == VenafiCloudKeypair:
if cfg.ClusterID == "" {
errs = multierror.Append(errs, fmt.Errorf("cluster_id is required in %s mode", res.AuthMode))
errs = multierror.Append(errs, fmt.Errorf("cluster_id is required in %s mode", res.TLSPKMode))
}
clusterID = cfg.ClusterID
case res.AuthMode == VenafiCloudVenafiConnection:
case res.TLSPKMode == VenafiCloudVenafiConnection:
if cfg.ClusterID == "" {
errs = multierror.Append(errs, fmt.Errorf("cluster_id is required in %s mode", res.AuthMode))
errs = multierror.Append(errs, fmt.Errorf("cluster_id is required in %s mode", res.TLSPKMode))
}
clusterID = cfg.ClusterID
case res.AuthMode == JetstackSecureOAuth || res.AuthMode == JetstackSecureAPIToken:
case res.TLSPKMode == JetstackSecureOAuth || res.TLSPKMode == JetstackSecureAPIToken:
if cfg.OrganizationID == "" {
errs = multierror.Append(errs, fmt.Errorf("organization_id is required"))
}
Expand Down Expand Up @@ -567,7 +648,7 @@ func ValidateAndCombineConfig(log logr.Logger, cfg Config, flags AgentCmdFlags)
res.InstallNS = installNS

// Validation of --venafi-connection and --venafi-connection-namespace.
if res.AuthMode == VenafiCloudVenafiConnection {
if res.TLSPKMode == VenafiCloudVenafiConnection {
res.VenConnName = flags.VenConnName
var venConnNS string = flags.VenConnNS
if flags.VenConnNS == "" {
Expand Down Expand Up @@ -634,7 +715,7 @@ func validateCredsAndCreateClient(log logr.Logger, flagCredentialsPath, flagClie
var preflightClient client.Client
metadata := &api.AgentMetadata{Version: version.PreflightVersion, ClusterID: cfg.ClusterID}
switch {
case cfg.AuthMode == JetstackSecureOAuth:
case cfg.TLSPKMode == JetstackSecureOAuth:
// Note that there are no command line flags to configure the
// JetstackSecureOAuth mode.
credsBytes, err := readCredentialsFile(flagCredentialsPath)
Expand All @@ -653,7 +734,7 @@ func validateCredsAndCreateClient(log logr.Logger, flagCredentialsPath, flagClie
if err != nil {
errs = multierror.Append(errs, err)
}
case cfg.AuthMode == VenafiCloudKeypair:
case cfg.TLSPKMode == VenafiCloudKeypair:
var creds client.Credentials

if flagClientID != "" && flagCredentialsPath != "" {
Expand Down Expand Up @@ -696,7 +777,7 @@ func validateCredsAndCreateClient(log logr.Logger, flagCredentialsPath, flagClie
if err != nil {
errs = multierror.Append(errs, err)
}
case cfg.AuthMode == VenafiCloudVenafiConnection:
case cfg.TLSPKMode == VenafiCloudVenafiConnection:
var restCfg *rest.Config
restCfg, err := kubeconfig.LoadRESTConfig("")
if err != nil {
Expand All @@ -708,18 +789,20 @@ func validateCredsAndCreateClient(log logr.Logger, flagCredentialsPath, flagClie
if err != nil {
errs = multierror.Append(errs, err)
}
case cfg.AuthMode == JetstackSecureAPIToken:
case cfg.TLSPKMode == JetstackSecureAPIToken:
var err error
preflightClient, err = client.NewAPITokenClient(metadata, flagAPIToken, cfg.Server)
if err != nil {
errs = multierror.Append(errs, err)
}
case cfg.TLSPKMode == Off:
// No client needed in this mode.
default:
panic(fmt.Errorf("programmer mistake: auth mode not implemented: %s", cfg.AuthMode))
panic(fmt.Errorf("programmer mistake: auth mode not implemented: %s", cfg.TLSPKMode))
}

if errs != nil {
return nil, fmt.Errorf("failed loading config using the %s mode: %w", cfg.AuthMode, errs)
return nil, fmt.Errorf("failed loading config using the %s mode: %w", cfg.TLSPKMode, errs)
}

return preflightClient, nil
Expand Down Expand Up @@ -756,7 +839,7 @@ func createCredentialClient(log logr.Logger, credentials client.Credentials, cfg
uploaderID := "no"

var uploadPath string
if cfg.AuthMode == VenafiCloudKeypair {
if cfg.TLSPKMode == VenafiCloudKeypair {
// We don't do this for the VenafiCloudVenafiConnection mode because
// the upload_path field is ignored in that mode.
log.Info("Loading upload_path from \"venafi-cloud\" configuration.")
Expand Down Expand Up @@ -846,7 +929,7 @@ func (c *Config) Dump() (string, error) {
d, err := yaml.Marshal(&c)

if err != nil {
return "", errors.Wrap(err, "failed to generate YAML dump of config")
return "", fmt.Errorf("failed to generate YAML dump of config: %w", err)
}

return string(d), nil
Expand Down
Loading