Skip to content
Open
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
5 changes: 5 additions & 0 deletions cmd/vault-subpath-proxy/kv_update_transport.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ type kvUpdateTransport struct {
// If enabled, the roundtripper will wait for secret
// sync to complete. Should only be enabled in tests.
synchronousSecretSync bool
readOnly bool

privilegedVaultClient *vaultclient.VaultClient
// existingSecretKeysByNamespaceName is used in the key validation.
Expand Down Expand Up @@ -84,6 +85,10 @@ func (k *kvUpdateTransport) RoundTrip(r *http.Request) (*http.Response, error) {
if (r.Method != http.MethodPut && r.Method != http.MethodPost && r.Method != http.MethodPatch && r.Method != http.MethodDelete) || !strings.HasPrefix(r.URL.Path, "/v1/"+k.kvMountPath) {
return k.upstream.RoundTrip(r)
}
if k.readOnly {
l.Warn("Rejected write operation: vault is in read-only mode")
return newResponse(http.StatusForbidden, r, "Vault is in read-only mode for migration. No secret modifications are allowed."), nil
}
if r.Method == http.MethodDelete {
resp, err := k.upstream.RoundTrip(r)
if err != nil || resp.StatusCode < 200 || resp.StatusCode > 299 {
Expand Down
11 changes: 8 additions & 3 deletions cmd/vault-subpath-proxy/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ type options struct {
kubernetesOptions flagutil.KubernetesOptions
vaultToken string
vaultRole string
readOnly bool
}

func gatherOptions() (*options, error) {
Expand All @@ -52,6 +53,7 @@ func gatherOptions() (*options, error) {
o.kubernetesOptions.AddFlags(fs)
fs.StringVar(&o.vaultToken, "vault-token", "", "Vault token that will be used to detect conflicting secrets. Must have read access to the whole kv store. Mutually exclusive with --vault-token.")
fs.StringVar(&o.vaultRole, "vault-role", "", "Vault role to use for detecting conflicting secrets. Must have access to the whole kv store. Mutually exclusive with --vault-token.")
fs.BoolVar(&o.readOnly, "read-only", false, "Reject all write operations to the KV store. Use during Vault-to-GSM migration to freeze secrets.")
if err := fs.Parse(os.Args[1:]); err != nil {
return nil, fmt.Errorf("failed to parse flags: %w", err)
}
Expand Down Expand Up @@ -92,7 +94,10 @@ func main() {
logrus.WithError(err).Fatal("failed to load kubeconfigs")
}

server, err := createProxyServer(opts.vaultAddr, opts.listenAddr, opts.kvMountPath, clientGetter, privilegedVaultClient)
if opts.readOnly {
logrus.Warn("Running in read-only mode: all write operations will be rejected")
}
server, err := createProxyServer(opts.vaultAddr, opts.listenAddr, opts.kvMountPath, clientGetter, privilegedVaultClient, opts.readOnly)
if err != nil {
logrus.WithError(err).Fatal("failed to create server")
}
Expand All @@ -111,7 +116,7 @@ func main() {
}
}

func createProxyServer(vaultAddr string, listenAddr string, kvMountPath string, clients func() map[string]ctrlruntimeclient.Client, privilegedVaultClient *vaultclient.VaultClient) (*http.Server, error) {
func createProxyServer(vaultAddr string, listenAddr string, kvMountPath string, clients func() map[string]ctrlruntimeclient.Client, privilegedVaultClient *vaultclient.VaultClient, readOnly bool) (*http.Server, error) {
vaultClient, err := api.NewClient(&api.Config{Address: vaultAddr})
if err != nil {
return nil, fmt.Errorf("failed to create vault client: %w", err)
Expand All @@ -122,7 +127,7 @@ func createProxyServer(vaultAddr string, listenAddr string, kvMountPath string,
}

proxy := httputil.NewSingleHostReverseProxy(vaultURL)
transport := &kvUpdateTransport{kvMountPath: kvMountPath, upstream: http.DefaultTransport, kubeClients: clients, privilegedVaultClient: privilegedVaultClient}
transport := &kvUpdateTransport{kvMountPath: kvMountPath, upstream: http.DefaultTransport, kubeClients: clients, privilegedVaultClient: privilegedVaultClient, readOnly: readOnly}
transport.initialize()
proxy.Transport = transport
injector := &kvSubPathInjector{
Expand Down
73 changes: 72 additions & 1 deletion cmd/vault-subpath-proxy/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ path "secret/metadata/team-1/*" {
}

proxyServerPort := testhelper.GetFreePort(t)
proxyServer, err := createProxyServer("http://"+vaultAddr, "127.0.0.1:"+proxyServerPort, "secret", nil, rootDirect)
proxyServer, err := createProxyServer("http://"+vaultAddr, "127.0.0.1:"+proxyServerPort, "secret", nil, rootDirect, false)
if err != nil {
t.Fatalf("failed to create proxy server: %v", err)
}
Expand Down Expand Up @@ -502,6 +502,77 @@ path "secret/metadata/team-1/*" {
}
})

t.Run("readOnlyMode", func(t *testing.T) {
kvUpdateTransport.readOnly = true
t.Cleanup(func() { kvUpdateTransport.readOnly = false })

readOnlyTestCases := []struct {
name string
operation string
path string
data map[string]string
expectedStatusCode int
expectedErrors []string
}{
{
name: "Write is rejected",
operation: "write",
path: "secret/read-only-test/should-fail",
data: map[string]string{"key": "value"},
expectedStatusCode: 403,
expectedErrors: []string{"Vault is in read-only mode for migration. No secret modifications are allowed."},
},
{
name: "Delete is rejected",
operation: "delete",
path: "secret/metadata/top-level",
expectedStatusCode: 403,
expectedErrors: []string{"Vault is in read-only mode for migration. No secret modifications are allowed."},
},
{
name: "Read still works",
operation: "read",
path: "secret/metadata",
},
}

for _, tc := range readOnlyTestCases {
t.Run(tc.name, func(t *testing.T) {
var actualStatusCode int
var actualErrors []string

var err error
switch tc.operation {
case "write":
err = rootProxy.UpsertKV(tc.path, tc.data)
case "delete":
_, err = rootProxy.Logical().Delete(tc.path)
case "read":
result, readErr := rootProxy.Logical().List(tc.path)
if readErr != nil {
t.Fatalf("expected read to succeed in read-only mode, got error: %v", readErr)
}
if result == nil || result.Data == nil {
t.Fatal("expected non-nil result for read in read-only mode")
}
}
if err != nil {
responseErr, ok := err.(*api.ResponseError)
if !ok {
t.Fatalf("got an error back that was not a response error but a %T", err)
}
actualStatusCode = responseErr.StatusCode
actualErrors = responseErr.Errors
}
if actualStatusCode != tc.expectedStatusCode {
t.Errorf("expected status code %d, got %d", tc.expectedStatusCode, actualStatusCode)
}
if diff := cmp.Diff(actualErrors, tc.expectedErrors); diff != "" {
t.Errorf("actual errors differ from expected: %s", diff)
}
})
}
})
}
func writeKV(client *api.Client, path string, data map[string]string) error {
request := client.NewRequest("POST", path)
Expand Down