Agent JWT auto auth remove_jwt_after_reading
config option (#11969)
Add a new config option for Vault Agent's JWT auto auth `remove_jwt_after_reading`, which defaults to true. Can stop Agent from attempting to delete the file, which is useful in k8s where the service account JWT is mounted as a read-only file and so any attempt to delete it generates spammy error logs. When leaving the JWT file in place, the read period for new tokens is 1 minute instead of 500ms to reflect the assumption that there will always be a file there, so finding a file does not provide any signal that it needs to be re-read. Kubernetes has a minimum TTL of 10 minutes for tokens, so a period of 1 minute gives Agent plenty of time to detect new tokens, without leaving it too unresponsive. We may want to add a config option to override these default periods in the future. Co-authored-by: Tom Proctor <tomhjp@users.noreply.github.com>
This commit is contained in:
parent
16c0e4cbd2
commit
887e77c2ae
3
changelog/11969.txt
Normal file
3
changelog/11969.txt
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:improvement
|
||||||
|
agent: JWT auto auth now supports a `remove_jwt_after_reading` config option which defaults to true.
|
||||||
|
```
|
|
@ -5,7 +5,6 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
|
@ -15,6 +14,7 @@ import (
|
||||||
hclog "github.com/hashicorp/go-hclog"
|
hclog "github.com/hashicorp/go-hclog"
|
||||||
"github.com/hashicorp/vault/api"
|
"github.com/hashicorp/vault/api"
|
||||||
"github.com/hashicorp/vault/command/agent/auth"
|
"github.com/hashicorp/vault/command/agent/auth"
|
||||||
|
"github.com/hashicorp/vault/sdk/helper/parseutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
type jwtMethod struct {
|
type jwtMethod struct {
|
||||||
|
@ -22,6 +22,7 @@ type jwtMethod struct {
|
||||||
path string
|
path string
|
||||||
mountPath string
|
mountPath string
|
||||||
role string
|
role string
|
||||||
|
removeJWTAfterReading bool
|
||||||
credsFound chan struct{}
|
credsFound chan struct{}
|
||||||
watchCh chan string
|
watchCh chan string
|
||||||
stopCh chan struct{}
|
stopCh chan struct{}
|
||||||
|
@ -45,6 +46,7 @@ func NewJWTAuthMethod(conf *auth.AuthConfig) (auth.AuthMethod, error) {
|
||||||
j := &jwtMethod{
|
j := &jwtMethod{
|
||||||
logger: conf.Logger,
|
logger: conf.Logger,
|
||||||
mountPath: conf.MountPath,
|
mountPath: conf.MountPath,
|
||||||
|
removeJWTAfterReading: true,
|
||||||
credsFound: make(chan struct{}),
|
credsFound: make(chan struct{}),
|
||||||
watchCh: make(chan string),
|
watchCh: make(chan string),
|
||||||
stopCh: make(chan struct{}),
|
stopCh: make(chan struct{}),
|
||||||
|
@ -73,6 +75,14 @@ func NewJWTAuthMethod(conf *auth.AuthConfig) (auth.AuthMethod, error) {
|
||||||
return nil, errors.New("could not convert 'role' config value to string")
|
return nil, errors.New("could not convert 'role' config value to string")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if removeJWTAfterReadingRaw, ok := conf.Config["remove_jwt_after_reading"]; ok {
|
||||||
|
removeJWTAfterReading, err := parseutil.ParseBool(removeJWTAfterReadingRaw)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error parsing 'remove_jwt_after_reading' value: %w", err)
|
||||||
|
}
|
||||||
|
j.removeJWTAfterReading = removeJWTAfterReading
|
||||||
|
}
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case j.path == "":
|
case j.path == "":
|
||||||
return nil, errors.New("'path' value is empty")
|
return nil, errors.New("'path' value is empty")
|
||||||
|
@ -80,7 +90,14 @@ func NewJWTAuthMethod(conf *auth.AuthConfig) (auth.AuthMethod, error) {
|
||||||
return nil, errors.New("'role' value is empty")
|
return nil, errors.New("'role' value is empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
j.ticker = time.NewTicker(500 * time.Millisecond)
|
// If we don't delete the JWT after reading, use a slower reload period,
|
||||||
|
// otherwise we would re-read the whole file every 500ms, instead of just
|
||||||
|
// doing a stat on the file every 500ms.
|
||||||
|
readPeriod := 1 * time.Minute
|
||||||
|
if j.removeJWTAfterReading {
|
||||||
|
readPeriod = 500 * time.Millisecond
|
||||||
|
}
|
||||||
|
j.ticker = time.NewTicker(readPeriod)
|
||||||
|
|
||||||
go j.runWatcher()
|
go j.runWatcher()
|
||||||
|
|
||||||
|
@ -145,6 +162,7 @@ func (j *jwtMethod) runWatcher() {
|
||||||
j.ingressToken()
|
j.ingressToken()
|
||||||
newToken := j.latestToken.Load().(string)
|
newToken := j.latestToken.Load().(string)
|
||||||
if newToken != latestToken {
|
if newToken != latestToken {
|
||||||
|
j.logger.Debug("new jwt file found")
|
||||||
j.credsFound <- struct{}{}
|
j.credsFound <- struct{}{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -161,11 +179,9 @@ func (j *jwtMethod) ingressToken() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
j.logger.Debug("new jwt file found")
|
|
||||||
|
|
||||||
// Check that the path refers to a file.
|
// Check that the path refers to a file.
|
||||||
// If it's a symlink, it could still be a symlink to a directory,
|
// If it's a symlink, it could still be a symlink to a directory,
|
||||||
// but ioutil.ReadFile below will return a descriptive error.
|
// but os.ReadFile below will return a descriptive error.
|
||||||
switch mode := fi.Mode(); {
|
switch mode := fi.Mode(); {
|
||||||
case mode.IsRegular():
|
case mode.IsRegular():
|
||||||
// regular file
|
// regular file
|
||||||
|
@ -176,7 +192,7 @@ func (j *jwtMethod) ingressToken() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
token, err := ioutil.ReadFile(j.path)
|
token, err := os.ReadFile(j.path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
j.logger.Error("failed to read jwt file", "error", err)
|
j.logger.Error("failed to read jwt file", "error", err)
|
||||||
return
|
return
|
||||||
|
@ -190,7 +206,9 @@ func (j *jwtMethod) ingressToken() {
|
||||||
j.latestToken.Store(string(token))
|
j.latestToken.Store(string(token))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if j.removeJWTAfterReading {
|
||||||
if err := os.Remove(j.path); err != nil {
|
if err := os.Remove(j.path); err != nil {
|
||||||
j.logger.Error("error removing jwt file", "error", err)
|
j.logger.Error("error removing jwt file", "error", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -2,7 +2,6 @@ package jwt
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -10,6 +9,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/hashicorp/go-hclog"
|
"github.com/hashicorp/go-hclog"
|
||||||
|
"github.com/hashicorp/vault/command/agent/auth"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestIngressToken(t *testing.T) {
|
func TestIngressToken(t *testing.T) {
|
||||||
|
@ -21,18 +21,18 @@ func TestIngressToken(t *testing.T) {
|
||||||
symlinked = "symlinked"
|
symlinked = "symlinked"
|
||||||
)
|
)
|
||||||
|
|
||||||
rootDir, err := ioutil.TempDir("", "vault-agent-jwt-auth-test")
|
rootDir, err := os.MkdirTemp("", "vault-agent-jwt-auth-test")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create temp dir: %s", err)
|
t.Fatalf("failed to create temp dir: %s", err)
|
||||||
}
|
}
|
||||||
defer os.RemoveAll(rootDir)
|
defer os.RemoveAll(rootDir)
|
||||||
|
|
||||||
setupTestDir := func() string {
|
setupTestDir := func() string {
|
||||||
testDir, err := ioutil.TempDir(rootDir, "")
|
testDir, err := os.MkdirTemp(rootDir, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
err = ioutil.WriteFile(path.Join(testDir, file), []byte("test"), 0o644)
|
err = os.WriteFile(path.Join(testDir, file), []byte("test"), 0o644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -106,3 +106,62 @@ func TestIngressToken(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDeleteAfterReading(t *testing.T) {
|
||||||
|
for _, tc := range map[string]struct {
|
||||||
|
configValue string
|
||||||
|
shouldDelete bool
|
||||||
|
}{
|
||||||
|
"default": {
|
||||||
|
"",
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
"explicit true": {
|
||||||
|
"true",
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
"false": {
|
||||||
|
"false",
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
rootDir, err := os.MkdirTemp("", "vault-agent-jwt-auth-test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create temp dir: %s", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(rootDir)
|
||||||
|
tokenPath := path.Join(rootDir, "token")
|
||||||
|
err = os.WriteFile(tokenPath, []byte("test"), 0o644)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
config := &auth.AuthConfig{
|
||||||
|
Config: map[string]interface{}{
|
||||||
|
"path": tokenPath,
|
||||||
|
"role": "unusedrole",
|
||||||
|
},
|
||||||
|
Logger: hclog.Default(),
|
||||||
|
}
|
||||||
|
if tc.configValue != "" {
|
||||||
|
config.Config["remove_jwt_after_reading"] = tc.configValue
|
||||||
|
}
|
||||||
|
|
||||||
|
jwtAuth, err := NewJWTAuthMethod(config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
jwtAuth.(*jwtMethod).ingressToken()
|
||||||
|
|
||||||
|
if _, err := os.Lstat(tokenPath); tc.shouldDelete {
|
||||||
|
if err == nil || !os.IsNotExist(err) {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -14,3 +14,7 @@ method](/docs/auth/jwt).
|
||||||
- `path` `(string: required)` - The path to the JWT file
|
- `path` `(string: required)` - The path to the JWT file
|
||||||
|
|
||||||
- `role` `(string: required)` - The role to authenticate against on Vault
|
- `role` `(string: required)` - The role to authenticate against on Vault
|
||||||
|
|
||||||
|
- `remove_jwt_after_reading` `(bool: optional, defaults to true)` -
|
||||||
|
This can be set to `false` to disable the default behavior of removing the
|
||||||
|
JWT after it's been read.
|
||||||
|
|
Loading…
Reference in a new issue