Skip to content
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
* [BUGFIX] Packaging: Fix RPM and deb packages to install the binary to `/usr/bin`, install the systemd unit to the correct system path (`/usr/lib/systemd/system` for RPM, `/lib/systemd/system` for deb), and mark the sysconfig/default env file as a config file so it is not overwritten on upgrade. #7445
* [BUGFIX] Compactor: Handle not-found and access-denied errors from `Attributes()` in bucket index updater, preventing a stale cached `Get()` from causing the entire cleanup cycle to fail when `meta.json` has been deleted from object storage. #7454
* [BUGFIX] gRPC: Fix panic when `grpc_compression` is set to `snappy` on ingester client or store-gateway client configurations. #7459
* [BUGFIX] Config: Mask Swift, etcd, Redis, and HTTP basic-auth credentials on the `/config` endpoint. #7473

## 1.21.0 2026-04-24

Expand Down
105 changes: 105 additions & 0 deletions pkg/cortex/modules_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import (
"context"
"net/http/httptest"
"os"
"reflect"
"regexp"
"strings"
"testing"

"github.com/gorilla/mux"
Expand All @@ -14,6 +17,7 @@ import (

"github.com/cortexproject/cortex/pkg/configs"
"github.com/cortexproject/cortex/pkg/cortexpb"
"github.com/cortexproject/cortex/pkg/util/flagext"
)

func changeTargetConfig(c *Config) {
Expand Down Expand Up @@ -248,3 +252,104 @@ func Test_initResourceMonitor_shouldFailOnInvalidResource(t *testing.T) {
_, err := cortex.initResourceMonitor()
require.ErrorContains(t, err, "unknown resource type")
}

func TestConfigEndpoint_SecretsMasked(t *testing.T) {
cfg := newDefaultConfig()

// Use reflection to find every flagext.Secret field in the config and set
// it to a sentinel value. This ensures newly added Secret fields are
// automatically covered without updating this test.
sentinel := "LEAKED_SECRET_VALUE"
setAllSecrets(reflect.ValueOf(cfg), sentinel)

cortex := &Cortex{
Server: &server.Server{},
Cfg: *cfg,
}
cortex.Server.HTTP = mux.NewRouter()

_, err := cortex.initAPI()
require.NoError(t, err)

req := httptest.NewRequest("GET", "/config", nil)
resp := httptest.NewRecorder()
cortex.Server.HTTP.ServeHTTP(resp, req)

require.Equal(t, 200, resp.Code)

body := resp.Body.String()

// Verify the sentinel never appears in cleartext.
assert.NotContains(t, body, sentinel, "a flagext.Secret value was leaked in /config output")

// Verify at least one masked value is present (sanity check).
assert.Contains(t, body, "********", "expected masked secrets in /config output")
}

// TestConfig_SensitiveFieldTypes verifies that every struct field in Config
// whose YAML tag name suggests a credential uses flagext.Secret, not string.
// This catches new password/secret fields added as plain strings even if they
// have no default value.
func TestConfig_SensitiveFieldTypes(t *testing.T) {
sensitivePattern := regexp.MustCompile(`(?i)^(password|secret|secret_key|application_credential_secret|basic_auth_password)$`)
secretType := reflect.TypeFor[flagext.Secret]()

var violations []string
checkSensitiveFields(reflect.TypeFor[Config](), "", sensitivePattern, secretType, &violations)

for _, v := range violations {
t.Errorf("field should use flagext.Secret, not string: %s", v)
}
}

// checkSensitiveFields recursively walks a type and reports any string field
// whose YAML tag matches the sensitive pattern.
func checkSensitiveFields(t reflect.Type, prefix string, pattern *regexp.Regexp, secretType reflect.Type, violations *[]string) {
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
if t.Kind() != reflect.Struct {
return
}
for f := range t.Fields() {
path := prefix + f.Name

yamlTag := f.Tag.Get("yaml")
yamlName := strings.Split(yamlTag, ",")[0]

if pattern.MatchString(yamlName) && f.Type.Kind() == reflect.String {
*violations = append(*violations, path+" (yaml:\""+yamlName+"\")")
}

ft := f.Type
if ft.Kind() == reflect.Ptr {
ft = ft.Elem()
}
if ft.Kind() == reflect.Struct && ft != secretType {
checkSensitiveFields(ft, path+".", pattern, secretType, violations)
}
}
}

// setAllSecrets recursively walks a reflect.Value and sets every flagext.Secret
// field's Value to the given sentinel string.
func setAllSecrets(v reflect.Value, sentinel string) {
switch v.Kind() {
case reflect.Ptr:
if !v.IsNil() {
setAllSecrets(v.Elem(), sentinel)
}
case reflect.Struct:
secretType := reflect.TypeFor[flagext.Secret]()
for _, f := range v.Fields() {
if !f.CanSet() {
continue
}
if f.Type() == secretType {
f.Set(reflect.ValueOf(flagext.Secret{Value: sentinel}))
} else {
setAllSecrets(f, sentinel)
}
}
}
}
10 changes: 5 additions & 5 deletions pkg/ring/kv/etcd/etcd.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ type Config struct {
EnableTLS bool `yaml:"tls_enabled"`
TLS cortextls.ClientConfig `yaml:",inline"`

UserName string `yaml:"username"`
Password string `yaml:"password"`
PermitWithoutStream bool `yaml:"ping-without-stream-allowed"`
UserName string `yaml:"username"`
Password flagext.Secret `yaml:"password"`
PermitWithoutStream bool `yaml:"ping-without-stream-allowed"`
}

// Clientv3Facade is a subset of all Etcd client operations that are required
Expand Down Expand Up @@ -59,7 +59,7 @@ func (cfg *Config) RegisterFlagsWithPrefix(f *flag.FlagSet, prefix string) {
f.IntVar(&cfg.MaxRetries, prefix+"etcd.max-retries", 10, "The maximum number of retries to do for failed ops.")
f.BoolVar(&cfg.EnableTLS, prefix+"etcd.tls-enabled", false, "Enable TLS.")
f.StringVar(&cfg.UserName, prefix+"etcd.username", "", "Etcd username.")
f.StringVar(&cfg.Password, prefix+"etcd.password", "", "Etcd password.")
f.Var(&cfg.Password, prefix+"etcd.password", "Etcd password.")
f.BoolVar(&cfg.PermitWithoutStream, prefix+"etcd.ping-without-stream-allowed", true, "Send Keepalive pings with no streams.")
cfg.TLS.RegisterFlagsWithPrefix(prefix+"etcd", f)
}
Expand Down Expand Up @@ -107,7 +107,7 @@ func New(cfg Config, codec codec.Codec, logger log.Logger) (*Client, error) {
PermitWithoutStream: cfg.PermitWithoutStream,
TLS: tlsConfig,
Username: cfg.UserName,
Password: cfg.Password,
Password: cfg.Password.Value,
})
if err != nil {
return nil, err
Expand Down
2 changes: 1 addition & 1 deletion pkg/ruler/notifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ func amConfigFromURL(rulerConfig *Config, url *url.URL, apiVersion config.Alertm
if rulerConfig.Notifier.BasicAuth.IsEnabled() {
amConfig.HTTPClientConfig.BasicAuth = &config_util.BasicAuth{
Username: rulerConfig.Notifier.BasicAuth.Username,
Password: config_util.Secret(rulerConfig.Notifier.BasicAuth.Password),
Password: config_util.Secret(rulerConfig.Notifier.BasicAuth.Password.Value),
}
}

Expand Down
3 changes: 2 additions & 1 deletion pkg/ruler/notifier_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/stretchr/testify/require"

"github.com/cortexproject/cortex/pkg/util"
"github.com/cortexproject/cortex/pkg/util/flagext"
)

func TestBuildNotifierConfig(t *testing.T) {
Expand Down Expand Up @@ -195,7 +196,7 @@ func TestBuildNotifierConfig(t *testing.T) {
Notifier: NotifierConfig{
BasicAuth: util.BasicAuth{
Username: "jacob",
Password: "test",
Password: flagext.Secret{Value: "test"},
},
},
},
Expand Down
4 changes: 2 additions & 2 deletions pkg/storage/bucket/swift/bucket_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@ func NewBucketClient(cfg Config, hedgedRoundTripper func(rt http.RoundTripper) h
AuthUrl: cfg.AuthURL,
ApplicationCredentialID: cfg.ApplicationCredentialID,
ApplicationCredentialName: cfg.ApplicationCredentialName,
ApplicationCredentialSecret: cfg.ApplicationCredentialSecret,
ApplicationCredentialSecret: cfg.ApplicationCredentialSecret.Value,
Username: cfg.Username,
UserDomainName: cfg.UserDomainName,
UserDomainID: cfg.UserDomainID,
UserId: cfg.UserID,
Password: cfg.Password,
Password: cfg.Password.Value,
DomainId: cfg.DomainID,
DomainName: cfg.DomainName,
ProjectID: cfg.ProjectID,
Expand Down
48 changes: 25 additions & 23 deletions pkg/storage/bucket/swift/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,33 @@ package swift
import (
"flag"
"time"

"github.com/cortexproject/cortex/pkg/util/flagext"
)

// Config holds the config options for Swift backend
type Config struct {
AuthVersion int `yaml:"auth_version"`
AuthURL string `yaml:"auth_url"`
ApplicationCredentialID string `yaml:"application_credential_id"`
ApplicationCredentialName string `yaml:"application_credential_name"`
ApplicationCredentialSecret string `yaml:"application_credential_secret"`
Username string `yaml:"username"`
UserDomainName string `yaml:"user_domain_name"`
UserDomainID string `yaml:"user_domain_id"`
UserID string `yaml:"user_id"`
Password string `yaml:"password"`
DomainID string `yaml:"domain_id"`
DomainName string `yaml:"domain_name"`
ProjectID string `yaml:"project_id"`
ProjectName string `yaml:"project_name"`
ProjectDomainID string `yaml:"project_domain_id"`
ProjectDomainName string `yaml:"project_domain_name"`
RegionName string `yaml:"region_name"`
ContainerName string `yaml:"container_name"`
MaxRetries int `yaml:"max_retries"`
ConnectTimeout time.Duration `yaml:"connect_timeout"`
RequestTimeout time.Duration `yaml:"request_timeout"`
AuthVersion int `yaml:"auth_version"`
AuthURL string `yaml:"auth_url"`
ApplicationCredentialID string `yaml:"application_credential_id"`
ApplicationCredentialName string `yaml:"application_credential_name"`
ApplicationCredentialSecret flagext.Secret `yaml:"application_credential_secret"`
Username string `yaml:"username"`
UserDomainName string `yaml:"user_domain_name"`
UserDomainID string `yaml:"user_domain_id"`
UserID string `yaml:"user_id"`
Password flagext.Secret `yaml:"password"`
DomainID string `yaml:"domain_id"`
DomainName string `yaml:"domain_name"`
ProjectID string `yaml:"project_id"`
ProjectName string `yaml:"project_name"`
ProjectDomainID string `yaml:"project_domain_id"`
ProjectDomainName string `yaml:"project_domain_name"`
RegionName string `yaml:"region_name"`
ContainerName string `yaml:"container_name"`
MaxRetries int `yaml:"max_retries"`
ConnectTimeout time.Duration `yaml:"connect_timeout"`
RequestTimeout time.Duration `yaml:"request_timeout"`
}

// RegisterFlags registers the flags for Swift storage
Expand All @@ -43,7 +45,7 @@ func (cfg *Config) RegisterFlagsWithPrefix(prefix string, f *flag.FlagSet) {
f.StringVar(&cfg.UserDomainName, prefix+"swift.user-domain-name", "", "OpenStack Swift user's domain name.")
f.StringVar(&cfg.UserDomainID, prefix+"swift.user-domain-id", "", "OpenStack Swift user's domain ID.")
f.StringVar(&cfg.UserID, prefix+"swift.user-id", "", "OpenStack Swift user ID.")
f.StringVar(&cfg.Password, prefix+"swift.password", "", "OpenStack Swift API key.")
f.Var(&cfg.Password, prefix+"swift.password", "OpenStack Swift API key.")
f.StringVar(&cfg.DomainID, prefix+"swift.domain-id", "", "OpenStack Swift user's domain ID.")
f.StringVar(&cfg.DomainName, prefix+"swift.domain-name", "", "OpenStack Swift user's domain name.")
f.StringVar(&cfg.ProjectID, prefix+"swift.project-id", "", "OpenStack Swift project ID (v2,v3 auth only).")
Expand All @@ -52,7 +54,7 @@ func (cfg *Config) RegisterFlagsWithPrefix(prefix string, f *flag.FlagSet) {
f.StringVar(&cfg.ProjectDomainName, prefix+"swift.project-domain-name", "", "Name of the OpenStack Swift project's domain (v3 auth only), only needed if it differs from the user domain.")
f.StringVar(&cfg.ApplicationCredentialID, prefix+"swift.application-credential-id", "", "OpenStack Swift application credential ID.")
f.StringVar(&cfg.ApplicationCredentialName, prefix+"swift.application-credential-name", "", "OpenStack Swift application credential name.")
f.StringVar(&cfg.ApplicationCredentialSecret, prefix+"swift.application-credential-secret", "", "OpenStack Swift application credential secret.")
f.Var(&cfg.ApplicationCredentialSecret, prefix+"swift.application-credential-secret", "OpenStack Swift application credential secret.")
f.StringVar(&cfg.RegionName, prefix+"swift.region-name", "", "OpenStack Swift Region to use (v2,v3 auth only).")
f.StringVar(&cfg.ContainerName, prefix+"swift.container-name", "", "Name of the OpenStack Swift container to put chunks in.")
f.IntVar(&cfg.MaxRetries, prefix+"swift.max-retries", 3, "Max retries on requests error.")
Expand Down
15 changes: 8 additions & 7 deletions pkg/storage/tsdb/redis_client_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,16 @@ import (
"github.com/thanos-io/thanos/pkg/cacheutil"
"github.com/thanos-io/thanos/pkg/model"

"github.com/cortexproject/cortex/pkg/util/flagext"
"github.com/cortexproject/cortex/pkg/util/tls"
)

type RedisClientConfig struct {
Addresses string `yaml:"addresses"`
Username string `yaml:"username"`
Password string `yaml:"password"`
DB int `yaml:"db"`
MasterName string `yaml:"master_name"`
Addresses string `yaml:"addresses"`
Username string `yaml:"username"`
Password flagext.Secret `yaml:"password"`
DB int `yaml:"db"`
MasterName string `yaml:"master_name"`

MaxGetMultiConcurrency int `yaml:"max_get_multi_concurrency"`
GetMultiBatchSize int `yaml:"get_multi_batch_size"`
Expand Down Expand Up @@ -45,7 +46,7 @@ type RedisClientConfig struct {
func (cfg *RedisClientConfig) RegisterFlagsWithPrefix(f *flag.FlagSet, prefix string) {
f.StringVar(&cfg.Addresses, prefix+"addresses", "", "Comma separated list of redis addresses. Supported prefixes are: dns+ (looked up as an A/AAAA query), dnssrv+ (looked up as a SRV query, dnssrvnoa+ (looked up as a SRV query, with no A/AAAA lookup made after that).")
f.StringVar(&cfg.Username, prefix+"username", "", "Redis username.")
f.StringVar(&cfg.Password, prefix+"password", "", "Redis password.")
f.Var(&cfg.Password, prefix+"password", "Redis password.")
f.IntVar(&cfg.DB, prefix+"db", 0, "Database to be selected after connecting to the server.")
f.DurationVar(&cfg.DialTimeout, prefix+"dial-timeout", time.Second*5, "Client dial timeout.")
f.DurationVar(&cfg.ReadTimeout, prefix+"read-timeout", time.Second*3, "Client read timeout.")
Expand Down Expand Up @@ -82,7 +83,7 @@ func (cfg *RedisClientConfig) ToRedisClientConfig() cacheutil.RedisClientConfig
return cacheutil.RedisClientConfig{
Addr: cfg.Addresses,
Username: cfg.Username,
Password: cfg.Password,
Password: cfg.Password.Value,
DB: cfg.DB,
MasterName: cfg.MasterName,
DialTimeout: cfg.DialTimeout,
Expand Down
10 changes: 6 additions & 4 deletions pkg/util/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import (
otlog "github.com/opentracing/opentracing-go/log"
yaml "gopkg.in/yaml.v2"
yamlv3 "gopkg.in/yaml.v3"

"github.com/cortexproject/cortex/pkg/util/flagext"
)

const messageSizeLargerErrFmt = "received message larger than max (%d vs %d)"
Expand All @@ -31,18 +33,18 @@ func IsRequestBodyTooLarge(err error) bool {

// BasicAuth configures basic authentication for HTTP clients.
type BasicAuth struct {
Username string `yaml:"basic_auth_username"`
Password string `yaml:"basic_auth_password"`
Username string `yaml:"basic_auth_username"`
Password flagext.Secret `yaml:"basic_auth_password"`
}

func (b *BasicAuth) RegisterFlagsWithPrefix(prefix string, f *flag.FlagSet) {
f.StringVar(&b.Username, prefix+"basic-auth-username", "", "HTTP Basic authentication username. It overrides the username set in the URL (if any).")
f.StringVar(&b.Password, prefix+"basic-auth-password", "", "HTTP Basic authentication password. It overrides the password set in the URL (if any).")
f.Var(&b.Password, prefix+"basic-auth-password", "HTTP Basic authentication password. It overrides the password set in the URL (if any).")
}

// IsEnabled returns false if basic authentication isn't enabled.
func (b BasicAuth) IsEnabled() bool {
return b.Username != "" || b.Password != ""
return b.Username != "" || b.Password.Value != ""
}

// WriteJSONResponse writes some JSON as a HTTP response.
Expand Down
Loading