Skip to content
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

vault: fix namespace reset for clients with unset namespace #23491

Merged
merged 1 commit into from
Jul 3, 2024
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
3 changes: 3 additions & 0 deletions .changelog/23491.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:bug
vault: Fixed a bug where requests to derive or renew tokens could be sent to the wrong namespace
```
41 changes: 6 additions & 35 deletions client/vaultclient/vaultclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import (

metrics "github.com/armon/go-metrics"
hclog "github.com/hashicorp/go-hclog"

"github.com/hashicorp/nomad/helper/useragent"
"github.com/hashicorp/nomad/nomad/structs"
"github.com/hashicorp/nomad/nomad/structs/config"
Expand Down Expand Up @@ -65,9 +64,6 @@ type VaultClient interface {
// endpoint, along with whether or not the token is renewable.
DeriveTokenWithJWT(context.Context, JWTLoginRequest) (string, bool, error)

// GetConsulACL fetches the Consul ACL token required for the task
GetConsulACL(string, string) (*vaultapi.Secret, error)

// RenewToken renews a token with the given increment and adds it to
// the min-heap for periodic renewal.
RenewToken(string, int) (<-chan error, error)
Expand Down Expand Up @@ -250,13 +246,11 @@ func (c *vaultClient) Stop() {
}

// unlockAndUnset is used to unset the vault token on the client, restore the
// client's namespace, and release the lock. Helper method for deferring a call
// that does both.
func (c *vaultClient) unlockAndUnset(previousNs string) {
// client's default configured namespace, and release the lock. Helper method
// for deferring a call that does both.
func (c *vaultClient) unlockAndUnset() {
c.client.SetToken("")
if previousNs != "" {
c.client.SetNamespace(previousNs)
}
c.client.SetNamespace(c.config.Namespace)
c.lock.Unlock()
}

Expand All @@ -273,7 +267,7 @@ func (c *vaultClient) DeriveToken(alloc *structs.Allocation, taskNames []string)
}

c.lock.Lock()
defer c.unlockAndUnset(c.client.Namespace())
defer c.unlockAndUnset()

// Use the token supplied to interact with vault
c.client.SetToken("")
Expand All @@ -297,7 +291,7 @@ func (c *vaultClient) DeriveTokenWithJWT(ctx context.Context, req JWTLoginReques
}

c.lock.Lock()
defer c.unlockAndUnset(c.client.Namespace())
defer c.unlockAndUnset()

// Make sure the login request is not passing any token and that we're using
// the expected namespace to login
Expand Down Expand Up @@ -330,29 +324,6 @@ func (c *vaultClient) DeriveTokenWithJWT(ctx context.Context, req JWTLoginReques
return s.Auth.ClientToken, s.Auth.Renewable, nil
}

// GetConsulACL creates a vault API client and reads from vault a consul ACL
// token used by the task.
func (c *vaultClient) GetConsulACL(token, path string) (*vaultapi.Secret, error) {
if !c.config.IsEnabled() {
return nil, fmt.Errorf("vault client not enabled")
}
if token == "" {
return nil, fmt.Errorf("missing token")
}
if path == "" {
return nil, fmt.Errorf("missing consul ACL token vault path")
}

c.lock.Lock()
defer c.unlockAndUnset(c.client.Namespace())

// Use the token supplied to interact with vault
c.client.SetToken(token)

// Read the consul ACL token and return the secret directly
return c.client.Logical().Read(path)
}

// RenewToken renews the supplied token for a given duration (in seconds) and
// adds it to the min-heap so that it is renewed periodically by the renewal
// loop. Any error returned during renewal will be written to a buffered
Expand Down
30 changes: 30 additions & 0 deletions client/vaultclient/vaultclient_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -677,3 +677,33 @@ func TestVaultClient_RenewalConcurrent(t *testing.T) {
}
}
}

func TestVaultClient_NamespaceReset(t *testing.T) {

// Mock Vault API that always returns an error
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintln(w, "error")
}))
defer ts.Close()

conf := structsc.DefaultVaultConfig()
conf.Addr = ts.URL
conf.Enabled = pointer.Of(true)

for _, ns := range []string{"", "foo"} {
conf.Namespace = ns

vc, err := NewVaultClient(conf, testlog.HCLogger(t), nil)
must.NoError(t, err)
vc.Start()

_, _, err = vc.DeriveTokenWithJWT(context.Background(), JWTLoginRequest{
JWT: "bogus",
Namespace: "bar",
})
must.Error(t, err)
must.Eq(t, ns, vc.client.Namespace(),
must.Sprintf("expected %q, not %q", ns, vc.client.Namespace()))
}
}
3 changes: 0 additions & 3 deletions client/vaultclient/vaultclient_testing.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (

"github.com/hashicorp/nomad/helper/uuid"
"github.com/hashicorp/nomad/nomad/structs"
vaultapi "github.com/hashicorp/vault/api"
)

// MockVaultClient is used for testing the vaultclient integration and is safe
Expand Down Expand Up @@ -162,8 +161,6 @@ func (vc *MockVaultClient) SetRenewable(renewable bool) {
vc.renewable = renewable
}

func (vc *MockVaultClient) GetConsulACL(string, string) (*vaultapi.Secret, error) { return nil, nil }

// LegacyTokens returns the tokens generated using the legacy flow.
func (vc *MockVaultClient) LegacyTokens() map[string]string {
vc.mu.Lock()
Expand Down