Skip to content

Commit

Permalink
fix: redact passwords under account-routes
Browse files Browse the repository at this point in the history
This commit makes changes so that all passwords and private keys are
redacted in logs, when json marshalled and when yaml marshalled.

Addresses: ENTERPRISE-4159

Signed-off-by: Bob Melander <[email protected]>
  • Loading branch information
bobmel committed Jul 8, 2024
1 parent 1d4a88b commit 1c94d42
Show file tree
Hide file tree
Showing 9 changed files with 348 additions and 176 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ require (
github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04
github.com/h2non/gock v1.2.0
github.com/mitchellh/go-homedir v1.1.0
github.com/nsf/jsondiff v0.0.0-20230430225905-43f6cf3098c1
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.8.1
github.com/spf13/viper v1.19.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4=
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
github.com/nsf/jsondiff v0.0.0-20230430225905-43f6cf3098c1 h1:dOYG7LS/WK00RWZc8XGgcUTlTxpp3mKhdR2Q9z9HbXM=
github.com/nsf/jsondiff v0.0.0-20230430225905-43f6cf3098c1/go.mod h1:mpRZBD8SJ55OIICQ3iWH0Yz3cjzA61JdqMLoWXeB2+8=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
Expand Down
153 changes: 86 additions & 67 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ are listed below in order of precedence:
*/package config

import (
"encoding/json"
"fmt"
"path"
"strings"

"gopkg.in/yaml.v2"

"github.com/anchore/k8s-inventory/pkg/mode"
"gopkg.in/yaml.v2"

"github.com/adrg/xdg"
"github.com/mitchellh/go-homedir"
Expand All @@ -37,106 +37,106 @@ type CliOnlyOptions struct {
// All Application configurations
type Application struct {
ConfigPath string
Quiet bool `mapstructure:"quiet"`
Log Logging `mapstructure:"log"`
Quiet bool `mapstructure:"quiet" json:"quiet,omitempty" yaml:"quiet"`
Log Logging `mapstructure:"log" json:"log,omitempty" yaml:"log"`
CliOptions CliOnlyOptions
Dev Development `mapstructure:"dev"`
KubeConfig KubeConf `mapstructure:"kubeconfig"`
Kubernetes KubernetesAPI `mapstructure:"kubernetes"`
Namespaces []string `mapstructure:"namespaces"`
KubernetesRequestTimeoutSeconds int64 `mapstructure:"kubernetes-request-timeout-seconds"`
NamespaceSelectors NamespaceSelector `mapstructure:"namespace-selectors"`
AccountRoutes AccountRoutes `mapstructure:"account-routes"`
AccountRouteByNamespaceLabel AccountRouteByNamespaceLabel `mapstructure:"account-route-by-namespace-label"`
MissingRegistryOverride string `mapstructure:"missing-registry-override"`
MissingTagPolicy MissingTagConf `mapstructure:"missing-tag-policy"`
Dev Development `mapstructure:"dev" json:"dev,omitempty" yaml:"dev"`
KubeConfig KubeConf `mapstructure:"kubeconfig" json:"kubeconfig,omitempty" yaml:"kubeconfig"`
Kubernetes KubernetesAPI `mapstructure:"kubernetes" json:"kubernetes,omitempty" yaml:"kubernetes"`
Namespaces []string `mapstructure:"namespaces" json:"namespaces,omitempty" yaml:"namespaces"`
KubernetesRequestTimeoutSeconds int64 `mapstructure:"kubernetes-request-timeout-seconds" json:"kubernetes-request-timeout-seconds,omitempty" yaml:"kubernetes-request-timeout-seconds"`
NamespaceSelectors NamespaceSelector `mapstructure:"namespace-selectors" json:"namespace-selectors,omitempty" yaml:"namespace-selectors"`
AccountRoutes AccountRoutes `mapstructure:"account-routes" json:"account-routes,omitempty" yaml:"account-routes"`
AccountRouteByNamespaceLabel AccountRouteByNamespaceLabel `mapstructure:"account-route-by-namespace-label" json:"account-route-by-namespace-label,omitempty" yaml:"account-route-by-namespace-label"`
MissingRegistryOverride string `mapstructure:"missing-registry-override" json:"missing-registry-override,omitempty" yaml:"missing-registry-override"`
MissingTagPolicy MissingTagConf `mapstructure:"missing-tag-policy" json:"missing-tag-policy,omitempty" yaml:"missing-tag-policy"`
RunMode mode.Mode
Mode string `mapstructure:"mode"`
IgnoreNotRunning bool `mapstructure:"ignore-not-running"`
PollingIntervalSeconds int `mapstructure:"polling-interval-seconds"`
InventoryReportLimits InventoryReportLimits `mapstructure:"inventory-report-limits"`
MetadataCollection MetadataCollection `mapstructure:"metadata-collection"`
AnchoreDetails AnchoreInfo `mapstructure:"anchore"`
VerboseInventoryReports bool `mapstructure:"verbose-inventory-reports"`
Mode string `mapstructure:"mode" json:"mode,omitempty" yaml:"mode"`
IgnoreNotRunning bool `mapstructure:"ignore-not-running" json:"ignore-not-running,omitempty" yaml:"ignore-not-running"`
PollingIntervalSeconds int `mapstructure:"polling-interval-seconds" json:"polling-interval-seconds,omitempty" yaml:"polling-interval-seconds"`
InventoryReportLimits InventoryReportLimits `mapstructure:"inventory-report-limits" json:"inventory-report-limits,omitempty" yaml:"inventory-report-limits"`
MetadataCollection MetadataCollection `mapstructure:"metadata-collection" json:"metadata-collection,omitempty" yaml:"metadata-collection"`
AnchoreDetails AnchoreInfo `mapstructure:"anchore" json:"anchore,omitempty" yaml:"anchore"`
VerboseInventoryReports bool `mapstructure:"verbose-inventory-reports" json:"verbose-inventory-reports,omitempty" yaml:"verbose-inventory-reports"`
}

// MissingTagConf details the policy for handling missing tags when reporting images
type MissingTagConf struct {
Policy string `mapstructure:"policy"`
Tag string `mapstructure:"tag,omitempty"`
Policy string `mapstructure:"policy" json:"policy,omitempty" yaml:"policy"`
Tag string `mapstructure:"tag,omitempty" json:"tag,omitempty" yaml:"tag"`
}

// NamespaceSelector details the inclusion/exclusion rules for namespaces
type NamespaceSelector struct {
Include []string `mapstructure:"include"`
Exclude []string `mapstructure:"exclude"`
IgnoreEmpty bool `mapstructure:"ignore-empty"`
Include []string `mapstructure:"include" json:"include,omitempty" yaml:"include"`
Exclude []string `mapstructure:"exclude" json:"exclude,omitempty" yaml:"exclude"`
IgnoreEmpty bool `mapstructure:"ignore-empty" json:"ignore-empty,omitempty" yaml:"ignore-empty"`
}

type AccountRoutes map[string]AccountRouteDetails

type AccountRouteDetails struct {
User string `mapstructure:"user"`
Password string `mapstructure:"password"`
Namespaces []string `mapstructure:"namespaces"`
User string `mapstructure:"user" json:"user,omitempty" yaml:"user"`
Password string `mapstructure:"password" json:"password,omitempty" yaml:"password"`
Namespaces []string `mapstructure:"namespaces" json:"namespaces,omitempty" yaml:"namespaces"`
}

type AccountRouteByNamespaceLabel struct {
LabelKey string `mapstructure:"key"`
DefaultAccount string `mapstructure:"default-account"`
IgnoreMissingLabel bool `mapstructure:"ignore-missing-label"`
LabelKey string `mapstructure:"key" json:"key,omitempty" yaml:"key"`
DefaultAccount string `mapstructure:"default-account" json:"default-account,omitempty" yaml:"default-account"`
IgnoreMissingLabel bool `mapstructure:"ignore-missing-label" json:"ignore-missing-label,omitempty" yaml:"ignore-missing-label"`
}

// KubernetesAPI details the configuration for interacting with the k8s api server
type KubernetesAPI struct {
RequestTimeoutSeconds int64 `mapstructure:"request-timeout-seconds"`
RequestBatchSize int64 `mapstructure:"request-batch-size"`
WorkerPoolSize int `mapstructure:"worker-pool-size"`
RequestTimeoutSeconds int64 `mapstructure:"request-timeout-seconds" json:"request-timeout-second,omitempty" yaml:"request-timeout-seconds"`
RequestBatchSize int64 `mapstructure:"request-batch-size" json:"request-batch-size,omitempty" yaml:"request-batch-size"`
WorkerPoolSize int `mapstructure:"worker-pool-size" json:"worker-pool-size,omitempty" yaml:"worker-pool-size"`
}

// Details upper limits for the inventory report contents before splitting into batches
type InventoryReportLimits struct {
Namespaces int `mapstructure:"namespaces"`
Namespaces int `mapstructure:"namespaces" json:"namespaces,omitempty" yaml:"namespaces"`
}

type ResourceMetadata struct {
Annotations []string `json:"include-annotations"`
Labels []string `json:"include-labels"`
Disable bool `json:"disable-all-metadata"`
Annotations []string `mapstructure:"include-annotations" json:"include-annotations,omitempty" yaml:"include-annotations"`
Labels []string `mapstructure:"include-labels" json:"include-labels,omitempty" yaml:"include-labels"`
Disable bool `mapstructure:"disable" json:"disable,omitempty" yaml:"disable"`
}

type MetadataCollection struct {
Nodes ResourceMetadata `mapstructure:"nodes"`
Namespace ResourceMetadata `mapstructure:"namespaces"`
Pods ResourceMetadata `mapstructure:"pods"`
Nodes ResourceMetadata `mapstructure:"nodes" json:"nodes,omitempty" yaml:"nodes"`
Namespace ResourceMetadata `mapstructure:"namespaces" json:"namespace,omitempty" yaml:"namespaces"`
Pods ResourceMetadata `mapstructure:"pods" json:"pods,omitempty" yaml:"pods"`
}

// Information for posting in-use image details to Anchore (or any URL for that matter)
type AnchoreInfo struct {
URL string `mapstructure:"url"`
User string `mapstructure:"user"`
Password string `mapstructure:"password"`
Account string `mapstructure:"account"`
HTTP HTTPConfig `mapstructure:"http"`
URL string `mapstructure:"url" json:"url,omitempty" yaml:"url"`
User string `mapstructure:"user" json:"user,omitempty" yaml:"user"`
Password string `mapstructure:"password" json:"password,omitempty" yaml:"password"`
Account string `mapstructure:"account" json:"account,omitempty" yaml:"account"`
HTTP HTTPConfig `mapstructure:"http" json:"http,omitempty" yaml:"http"`
}

// Configurations for the HTTP Client itself (net/http)
type HTTPConfig struct {
Insecure bool `mapstructure:"insecure"`
TimeoutSeconds int `mapstructure:"timeout-seconds"`
Insecure bool `mapstructure:"insecure" json:"insecure,omitempty" yaml:"insecure"`
TimeoutSeconds int `mapstructure:"timeout-seconds" json:"timeout-seconds,omitempty" yaml:"timeout-seconds"`
}

// Logging Configuration
type Logging struct {
Structured bool `mapstructure:"structured"`
Structured bool `mapstructure:"structured" json:"structured,omitempty" yaml:"structured"`
LevelOpt logrus.Level
Level string `mapstructure:"level"`
FileLocation string `mapstructure:"file"`
Level string `mapstructure:"level" json:"level,omitempty" yaml:"level"`
FileLocation string `mapstructure:"file" json:"file,omitempty" yaml:"file"`
}

// Development Configuration (only profile-cpu at the moment)
type Development struct {
ProfileCPU bool `mapstructure:"profile-cpu"`
ProfileCPU bool `mapstructure:"profile-cpu" json:"profile-cpu,omitempty" yaml:"profile-cpu"`
}

// Return whether or not AnchoreDetails are specified
Expand Down Expand Up @@ -334,26 +334,45 @@ func readConfig(v *viper.Viper, configPath string) error {
}

func (cfg Application) String() string {
// redact sensitive information
// Note: If the configuration grows to have more redacted fields it would be good to refactor this into something that
// is more dynamic based on a property or list of "sensitive" fields
if cfg.AnchoreDetails.Password != "" {
cfg.AnchoreDetails.Password = redacted
// yaml is pretty human friendly (at least when compared to json)
appCfgStr, err := yaml.Marshal(&cfg)
if err != nil {
return err.Error()
}

if cfg.KubeConfig.User.PrivateKey != "" {
cfg.KubeConfig.User.PrivateKey = redacted
return string(appCfgStr)
}

func (anchore AnchoreInfo) MarshalJSON() ([]byte, error) {
type achoreInfoAlias AnchoreInfo // prevent recursion

aIA := achoreInfoAlias(anchore)
if aIA.Password != "" {
aIA.Password = redacted
}
return json.Marshal(aIA)
}

if cfg.KubeConfig.User.Token != "" {
cfg.KubeConfig.User.Token = redacted
func (anchore AnchoreInfo) MarshalYAML() (interface{}, error) {
if anchore.Password != "" {
anchore.Password = redacted
}
return anchore, nil
}

// yaml is pretty human friendly (at least when compared to json)
appCfgStr, err := yaml.Marshal(&cfg)
if err != nil {
return err.Error()
func (aRD AccountRouteDetails) MarshalJSON() ([]byte, error) {
type AccountRouteDetailsAlias AccountRouteDetails // prevent recursion

aRDA := AccountRouteDetailsAlias(aRD)
if aRDA.Password != "" {
aRDA.Password = redacted
}
return json.Marshal(aRDA)
}

return string(appCfgStr)
func (aRD AccountRouteDetails) MarshalYAML() (interface{}, error) {
if aRD.Password != "" {
aRD.Password = redacted
}
return aRD, nil
}
51 changes: 51 additions & 0 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package config

import (
"encoding/json"
"flag"
"testing"

"github.com/anchore/go-testutils"
"github.com/nsf/jsondiff"
"github.com/spf13/viper"
)

Expand Down Expand Up @@ -55,6 +57,16 @@ func TestSensitiveConfigString(t *testing.T) {
config.AnchoreDetails.Password = "foo"
config.KubeConfig.User.PrivateKey = "baz"
config.KubeConfig.User.Token = "bar"
config.AccountRoutes["account0"] = AccountRouteDetails{
User: "account0User",
Password: "tooSimple",
Namespaces: []string{"ns-account0"},
}
config.AccountRoutes["account2"] = AccountRouteDetails{
User: "account2User",
Password: "notmuchbetter",
Namespaces: []string{"ns-account2"},
}
actual := config.String()

if *update {
Expand Down Expand Up @@ -119,3 +131,42 @@ func TestAnchoreInfo_IsValid(t *testing.T) {
})
}
}

func TestSensitiveConfigJSON(t *testing.T) {
config, err := LoadConfigFromFile(viper.GetViper(), &CliOnlyOptions{
ConfigPath: "../../anchore-k8s-inventory.yaml",
})
if err != nil {
t.Errorf("failed to load application config: \n\t%+v\n", err)
}
config.AnchoreDetails.Password = "foo"
config.KubeConfig.User.PrivateKey = "baz"
config.KubeConfig.User.Token = "bar"
config.AccountRoutes["account0"] = AccountRouteDetails{
User: "account0User",
Password: "tooSimple",
Namespaces: []string{"ns-account0"},
}
config.AccountRoutes["account2"] = AccountRouteDetails{
User: "account2User",
Password: "notmuchbetter",
Namespaces: []string{"ns-account2"},
}
actual, err := json.MarshalIndent(config, "", " ")
if err != nil {
t.Errorf("failed to marshal AnchoreInfo object: \n\t%+v\n", err)
}

if *update {
t.Logf("Updating Golden file")
testutils.UpdateGoldenFileContents(t, actual)
}

expected := string(testutils.GetGoldenFileContents(t))
diffOpts := jsondiff.DefaultConsoleOptions()
res, _ := jsondiff.Compare([]byte(expected), actual, &diffOpts)

if res != jsondiff.FullMatch {
t.Errorf("Config string does not match expected\nactual: %s\nexpected: %s", actual, expected)
}
}
43 changes: 33 additions & 10 deletions internal/config/kube_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,52 @@ package config

import (
"encoding/base64"
"encoding/json"
"fmt"

"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/tools/clientcmd/api"
)

// Defines how the Kubernetes Client should be configured. Note: Doesn't seem to work well with Env vars
type KubeConf struct {
Path string `mapstructure:"path"`
Cluster string `mapstructure:"cluster"`
ClusterCert string `mapstructure:"cluster-cert"`
Server string `mapstructure:"server"`
User KubeConfUser `mapstructure:"user"`
Path string `mapstructure:"path" json:"path,omitempty" yaml:"path"`
Cluster string `mapstructure:"cluster" json:"cluster,omitempty" yaml:"cluster"`
ClusterCert string `mapstructure:"cluster-cert" json:"cluster-cert,omitempty" yaml:"cluster-cert"`
Server string `mapstructure:"server" json:"server,omitempty" yaml:"server"`
User KubeConfUser `mapstructure:"user" json:"user,omitempty" yaml:"user"`
}

// If we are explicitly providing authentication information (not from a kubeconfig file), we need this info
type KubeConfUser struct {
UserConfType UserConf
UserConf string `mapstructure:"type"`
ClientCert string `mapstructure:"client-cert"`
PrivateKey string `mapstructure:"private-key"`
Token string `mapstructure:"token"`
UserConf string `mapstructure:"type" json:"type,omitempty" yaml:"type"`
ClientCert string `mapstructure:"client-cert" json:"client-cert,omitempty" yaml:"client-cert"`
PrivateKey string `mapstructure:"private-key" json:"private-key,omitempty" yaml:"private-key"`
Token string `mapstructure:"token" json:"token,omitempty" yaml:"token"`
}

func (user KubeConfUser) MarshalJSON() ([]byte, error) {
type kubeConfAlias KubeConfUser // prevent recursion

kCUA := kubeConfAlias(user)
if kCUA.PrivateKey != "" {
kCUA.PrivateKey = redacted
}
if kCUA.Token != "" {
kCUA.Token = redacted
}
return json.Marshal(kCUA)
}

func (user KubeConfUser) MarshalYAML() (interface{}, error) {
if user.PrivateKey != "" {
user.PrivateKey = redacted
}
if user.Token != "" {
user.Token = redacted
}
return user, nil
}

func (kubeConf *KubeConf) IsKubeConfigFromFile() bool {
Expand Down
Loading

0 comments on commit 1c94d42

Please sign in to comment.