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

Support ldap authentication in vault agent #21641

Merged
merged 3 commits into from
Aug 14, 2023
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/21641.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:feature
auto-auth: support ldap auth
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@VioletHynes or @marcboudreau would one of you be able to update this file to use the right format for a new feature (see the changelog process page) or change the section to an improvement?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll get this fixed shortly :)

```
259 changes: 259 additions & 0 deletions command/agentproxyshared/auth/ldap/ldap.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package ldap

import (
"context"
"errors"
"fmt"
"io/fs"
"net/http"
"os"
"path/filepath"
"sync"
"sync/atomic"
"time"

hclog "github.com/hashicorp/go-hclog"
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/command/agentproxyshared/auth"
"github.com/hashicorp/vault/sdk/helper/parseutil"
)

type ldapMethod struct {
logger hclog.Logger
mountPath string

username string
passwordFilePath string
removePasswordAfterReading bool
removePasswordFollowsSymlinks bool
credsFound chan struct{}
watchCh chan string
stopCh chan struct{}
doneCh chan struct{}
credSuccessGate chan struct{}
ticker *time.Ticker
once *sync.Once
latestPass *atomic.Value
}

// NewLdapMethod reads the user configuration and returns a configured
// LdapAuthMethod
func NewLdapAuthMethod(conf *auth.AuthConfig) (auth.AuthMethod, error) {
if conf == nil {
return nil, errors.New("empty config")
}
if conf.Config == nil {
return nil, errors.New("empty config data")
}

k := &ldapMethod{
logger: conf.Logger,
mountPath: conf.MountPath,
removePasswordAfterReading: true,
credsFound: make(chan struct{}),
watchCh: make(chan string),
stopCh: make(chan struct{}),
doneCh: make(chan struct{}),
credSuccessGate: make(chan struct{}),
once: new(sync.Once),
latestPass: new(atomic.Value),
}

k.latestPass.Store("")
usernameRaw, ok := conf.Config["username"]
if !ok {
return nil, errors.New("missing 'username' value")
}
k.username, ok = usernameRaw.(string)
if !ok {
return nil, errors.New("could not convert 'username' config value to string")
}

passFilePathRaw, ok := conf.Config["password_file_path"]
if !ok {
return nil, errors.New("missing 'password_file_path' value")
}
k.passwordFilePath, ok = passFilePathRaw.(string)
if !ok {
return nil, errors.New("could not convert 'password_file_path' config value to string")
}
if removePassAfterReadingRaw, ok := conf.Config["remove_password_after_reading"]; ok {
removePassAfterReading, err := parseutil.ParseBool(removePassAfterReadingRaw)
if err != nil {
return nil, fmt.Errorf("error parsing 'remove_password_after_reading' value: %w", err)
}
k.removePasswordAfterReading = removePassAfterReading
}

if removePassFollowsSymlinksRaw, ok := conf.Config["remove_password_follows_symlinks"]; ok {
removePassFollowsSymlinks, err := parseutil.ParseBool(removePassFollowsSymlinksRaw)
if err != nil {
return nil, fmt.Errorf("error parsing 'remove_password_follows_symlinks' value: %w", err)
}
k.removePasswordFollowsSymlinks = removePassFollowsSymlinks
}
switch {
case k.passwordFilePath == "":
return nil, errors.New("'password_file_path' value is empty")
case k.username == "":
return nil, errors.New("'username' value is empty")
}

// Default readPeriod
readPeriod := 1 * time.Minute

if passReadPeriodRaw, ok := conf.Config["password_read_period"]; ok {
passReadPeriod, err := parseutil.ParseDurationSecond(passReadPeriodRaw)
if err != nil {
return nil, fmt.Errorf("error parsing 'pass_read_period' value: %w", err)
}
readPeriod = passReadPeriod
} else {
// If we don't delete the password 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.
if k.removePasswordAfterReading {
readPeriod = 500 * time.Millisecond
}
}

k.ticker = time.NewTicker(readPeriod)

go k.runWatcher()

k.logger.Info("ldap auth method created", "password_file_path", k.passwordFilePath)

return k, nil
}

func (k *ldapMethod) Authenticate(ctx context.Context, client *api.Client) (string, http.Header, map[string]interface{}, error) {
k.logger.Trace("beginning authentication")

k.ingressPass()

latestPass := k.latestPass.Load().(string)

if latestPass == "" {
return "", nil, nil, errors.New("latest known password is empty, cannot authenticate")
}
k.logger.Info("last known password in Authentication setup is")
return fmt.Sprintf("%s/login/%s", k.mountPath, k.username), nil, map[string]interface{}{
"password": latestPass,
}, nil
}

func (k *ldapMethod) NewCreds() chan struct{} {
return k.credsFound
}

func (k *ldapMethod) CredSuccess() {
k.once.Do(func() {
close(k.credSuccessGate)
})
}

func (k *ldapMethod) Shutdown() {
k.ticker.Stop()
close(k.stopCh)
<-k.doneCh
}

func (k *ldapMethod) runWatcher() {
defer close(k.doneCh)

select {
case <-k.stopCh:
return

case <-k.credSuccessGate:
// We only start the next loop once we're initially successful,
// since at startup Authenticate will be called, and we don't want
// to end up immediately re-authenticating by having found a new
// value
}

for {
select {
case <-k.stopCh:
return

case <-k.ticker.C:
latestPass := k.latestPass.Load().(string)
k.ingressPass()
newPass := k.latestPass.Load().(string)
if newPass != latestPass {
k.logger.Debug("new password file found")
k.credsFound <- struct{}{}
}
}
}
}

func (k *ldapMethod) ingressPass() {
fi, err := os.Lstat(k.passwordFilePath)
if err != nil {
if os.IsNotExist(err) {
return
}
k.logger.Error("error encountered stat'ing password file", "error", err)
return
}

// Check that the path refers to a file.
// If it's a symlink, it could still be a symlink to a directory,
// but os.ReadFile below will return a descriptive error.
evalSymlinkPath := k.passwordFilePath
switch mode := fi.Mode(); {
case mode.IsRegular():
// regular file
case mode&fs.ModeSymlink != 0:
// If our file path is a symlink, we should also return early (like above) without error
// if the file that is linked to is not present, otherwise we will error when trying
// to read that file by following the link in the os.ReadFile call.
evalSymlinkPath, err = filepath.EvalSymlinks(k.passwordFilePath)
if err != nil {
k.logger.Error("error encountered evaluating symlinks", "error", err)
return
}
_, err := os.Stat(evalSymlinkPath)
if err != nil {
if os.IsNotExist(err) {
return
}
k.logger.Error("error encountered stat'ing password file after evaluating symlinks", "error", err)
return
}
default:
k.logger.Error("password file is not a regular file or symlink")
return
}

pass, err := os.ReadFile(k.passwordFilePath)
if err != nil {
k.logger.Error("failed to read password file", "error", err)
return
}

switch len(pass) {
case 0:
k.logger.Warn("empty password file read")

default:
k.latestPass.Store(string(pass))
}

if k.removePasswordAfterReading {
pathToRemove := k.passwordFilePath
if k.removePasswordFollowsSymlinks {
// If removePassFollowsSymlinks is set, we follow the symlink and delete the password,
// not just the symlink that links to the password file
pathToRemove = evalSymlinkPath
}
if err := os.Remove(pathToRemove); err != nil {
k.logger.Error("error removing password file", "error", err)
}
}
}
Loading