Skip to content

Commit 76db459

Browse files
committed
fix(webhook): validate object-supplied vault-addr and gate skip-verify
Signed-off-by: Bence Csati <bence.csati@axoflow.com>
1 parent 71cfbb8 commit 76db459

3 files changed

Lines changed: 102 additions & 1 deletion

File tree

pkg/webhook/config.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import (
3030
// VaultConfig represents vault options
3131
type VaultConfig struct {
3232
Addr string
33+
AddrFromObject bool
3334
AuthMethod string
3435
Role string
3536
Path string
@@ -106,6 +107,7 @@ func parseVaultConfig(obj metav1.Object, ar *model.AdmissionReview) VaultConfig
106107

107108
if val, ok := annotations[common.VaultAddrAnnotation]; ok {
108109
vaultConfig.Addr = val
110+
vaultConfig.AddrFromObject = true
109111
} else {
110112
vaultConfig.Addr = viper.GetString("vault_addr")
111113
}
@@ -145,7 +147,7 @@ func parseVaultConfig(obj metav1.Object, ar *model.AdmissionReview) VaultConfig
145147
}
146148

147149
if val, ok := annotations[common.VaultSkipVerifyAnnotation]; ok {
148-
vaultConfig.SkipVerify, _ = strconv.ParseBool(val)
150+
vaultConfig.SkipVerify = common.ResolveObjectSkipVerify(val, "vault_allow_object_skip_verify", "vault_skip_verify")
149151
} else {
150152
vaultConfig.SkipVerify = viper.GetBool("vault_skip_verify")
151153
}
@@ -473,6 +475,9 @@ func SetConfigDefaults() {
473475
viper.SetDefault("vault_ct_pull_policy", string(corev1.PullIfNotPresent))
474476
viper.SetDefault("vault_addr", "https://vault:8200")
475477
viper.SetDefault("vault_skip_verify", "false")
478+
viper.SetDefault("vault_addr_allowlist", "")
479+
viper.SetDefault("vault_allow_object_skip_verify", "false")
480+
viper.SetDefault("vault_allow_private_addr", "false")
476481
viper.SetDefault("vault_path", "kubernetes")
477482
viper.SetDefault("vault_auth_method", "jwt")
478483
viper.SetDefault("vault_role", "")

pkg/webhook/webhook.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import (
3232
"github.com/slok/kubewebhook/v2/pkg/log"
3333
"github.com/slok/kubewebhook/v2/pkg/model"
3434
"github.com/slok/kubewebhook/v2/pkg/webhook/mutating"
35+
"github.com/spf13/viper"
3536
authenticationv1 "k8s.io/api/authentication/v1"
3637
corev1 "k8s.io/api/core/v1"
3738
apierrors "k8s.io/apimachinery/pkg/api/errors"
@@ -193,6 +194,18 @@ func (mw *MutatingWebhook) lookForValueFrom(ctx context.Context, env corev1.EnvV
193194
return nil, nil
194195
}
195196

197+
func vaultAddrPolicy() common.AddrPolicy {
198+
allowlist := common.SplitAndTrim(viper.GetString("vault_addr_allowlist"))
199+
if addr := viper.GetString("vault_addr"); addr != "" {
200+
allowlist = append(allowlist, addr) // trusted operator default is implicitly allowed
201+
}
202+
203+
return common.AddrPolicy{
204+
Allowlist: allowlist,
205+
AllowPrivate: viper.GetBool("vault_allow_private_addr"),
206+
}
207+
}
208+
196209
func (mw *MutatingWebhook) newVaultClient(ctx context.Context, vaultConfig VaultConfig) (*vault.Client, error) {
197210
vaultAuthAttemptsCount.WithLabelValues().Inc()
198211
clientConfig := vaultapi.DefaultConfig()
@@ -201,6 +214,14 @@ func (mw *MutatingWebhook) newVaultClient(ctx context.Context, vaultConfig Vault
201214
return nil, clientConfig.Error
202215
}
203216

217+
// Validate before any connection or ServiceAccount token mint.
218+
if vaultConfig.AddrFromObject {
219+
if err := common.ValidateObjectAddr(vaultConfig.Addr, vaultAddrPolicy()); err != nil {
220+
vaultAuthAttemptsErrorsCount.WithLabelValues("config_error").Inc()
221+
return nil, errors.Wrap(err, "rejected Vault address from object annotation")
222+
}
223+
}
224+
204225
clientConfig.Address = vaultConfig.Addr
205226

206227
tlsConfig := vaultapi.TLSConfig{Insecure: vaultConfig.SkipVerify}

pkg/webhook/webhook_test.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323

2424
"github.com/prometheus/client_golang/prometheus"
2525
"github.com/prometheus/client_golang/prometheus/testutil"
26+
"github.com/spf13/viper"
2627
"github.com/stretchr/testify/assert"
2728
"github.com/stretchr/testify/require"
2829
corev1 "k8s.io/api/core/v1"
@@ -134,3 +135,77 @@ func TestNewVaultClientMetrics(t *testing.T) {
134135
})
135136
}
136137
}
138+
139+
func TestNewVaultClientRejectsObjectAddr(t *testing.T) {
140+
logger := slog.New(slog.DiscardHandler)
141+
require.NoError(t, os.Setenv("KUBERNETES_NAMESPACE", "test-namespace"))
142+
143+
tests := []struct {
144+
name string
145+
addr string
146+
allowlist string
147+
wantErr bool
148+
}{
149+
{
150+
name: "PoC IMDS address from object annotation is rejected",
151+
addr: "http://169.254.169.254/latest/meta-data/",
152+
wantErr: true,
153+
},
154+
{
155+
name: "non-allowlisted external address from object annotation is rejected",
156+
addr: "https://evil.attacker.com",
157+
wantErr: true,
158+
},
159+
{
160+
name: "userinfo-bearing address from object annotation is rejected",
161+
addr: "https://attacker:pw@vault.prod.svc:8200",
162+
allowlist: "https://vault.prod.svc:8200",
163+
wantErr: true,
164+
},
165+
{
166+
name: "allowlisted address from object annotation passes validation",
167+
addr: "https://vault.prod.svc:8200",
168+
allowlist: "https://vault.prod.svc:8200",
169+
wantErr: false,
170+
},
171+
}
172+
173+
for _, tt := range tests {
174+
t.Run(tt.name, func(t *testing.T) {
175+
vaultAuthAttemptsCount.Reset()
176+
vaultAuthAttemptsErrorsCount.Reset()
177+
viper.Set("vault_addr_allowlist", tt.allowlist)
178+
t.Cleanup(viper.Reset)
179+
180+
mw, err := NewMutatingWebhook(logger, fake.NewClientset())
181+
require.NoError(t, err)
182+
183+
vaultConfig := VaultConfig{
184+
Addr: tt.addr,
185+
AddrFromObject: true,
186+
SkipVerify: true,
187+
Role: "test-role",
188+
Path: "kubernetes",
189+
VaultServiceAccount: "high-priv-sa",
190+
ObjectNamespace: "test-namespace",
191+
}
192+
193+
_, err = mw.newVaultClient(t.Context(), vaultConfig)
194+
195+
if tt.wantErr {
196+
require.Error(t, err)
197+
assert.Contains(t, err.Error(), "rejected Vault address from object annotation")
198+
assert.Equal(t, float64(1), testutil.ToFloat64(vaultAuthAttemptsErrorsCount.WithLabelValues("config_error")),
199+
"address rejection must record a config_error")
200+
assert.Equal(t, float64(0), testutil.ToFloat64(vaultAuthAttemptsErrorsCount.WithLabelValues("kubernetes_error")),
201+
"rejection must happen before the ServiceAccount token path")
202+
} else {
203+
if err != nil {
204+
assert.NotContains(t, err.Error(), "rejected Vault address from object annotation")
205+
}
206+
assert.Equal(t, float64(0), testutil.ToFloat64(vaultAuthAttemptsErrorsCount.WithLabelValues("config_error")),
207+
"a valid allowlisted address must not record a config_error")
208+
}
209+
})
210+
}
211+
}

0 commit comments

Comments
 (0)