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
9 changes: 8 additions & 1 deletion cmd/authd/daemon/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ const cmdName = "authd"
// oldDBDir is the path of the old DB directory.
var oldDBDir = consts.OldDBDir

// pamDDirs are the directories containing PAM service configuration files.
var pamDDirs = []string{"/etc/pam.d", "/usr/lib/pam.d"}

// App encapsulate commands and options of the daemon, which can be controlled by env variables and config files.
type App struct {
rootCmd cobra.Command
Expand All @@ -47,7 +50,7 @@ type daemonConfig struct {
Verbosity int
Paths systemPaths
UsersConfig *users.Config `mapstructure:",squash" yaml:",inline"`
PAMConfig *pam.Config `mapstructure:",squash" yaml:",inline"`
PAMConfig *pam.Config `mapstructure:"pam" yaml:"pam"`
}

type options struct {
Expand Down Expand Up @@ -107,6 +110,10 @@ func New(args ...Option) *App {
setVerboseMode(a.config.Verbosity)
log.Debugf(context.Background(), "Verbosity: %d", a.config.Verbosity)

if a.config.PAMConfig != nil {
a.config.PAMConfig.WarnOnUnknownServices(context.Background(), pamDDirs)
}

// If we are only checking the configuration, we exit now.
if check, _ := cmd.Flags().GetBool("check-config"); check {
return nil
Expand Down
2 changes: 2 additions & 0 deletions cmd/authd/daemon/daemon_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/canonical/authd/cmd/authd/daemon"
"github.com/canonical/authd/internal/consts"
"github.com/canonical/authd/internal/fileutils"
"github.com/canonical/authd/internal/services/pam"
"github.com/canonical/authd/internal/testutils"
"github.com/canonical/authd/internal/users"
userslocking "github.com/canonical/authd/internal/users/locking"
Expand Down Expand Up @@ -311,6 +312,7 @@ func TestNoConfigSetDefaults(t *testing.T) {
require.Equal(t, consts.DefaultBrokersConfPath, a.Config().Paths.BrokersConf, "Default brokers configuration path")
require.Equal(t, consts.DefaultDatabaseDir, a.Config().Paths.Database, "Default database directory")
require.Equal(t, &users.DefaultConfig, a.Config().UsersConfig, "Default Users Config")
require.Equal(t, &pam.DefaultConfig, a.Config().PAMConfig, "Default PAM Config")
require.Equal(t, "", a.Config().Paths.Socket, "No socket address as default")
}

Expand Down
34 changes: 21 additions & 13 deletions debian/authd-config/authd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,27 @@
#GID_MIN: 10000
#GID_MAX: 60000

## Brute-force mitigation settings for authentication failures.
## To disable brute-force mitigation entirely, set auth_fail_delay to 0.
## PAM service settings.
##
## auth_fail_delay_threshold: number of consecutive failures for a single user
## before a delay is imposed on subsequent attempts.
#auth_fail_delay_threshold: 3
## Brute-force mitigation settings applied to all PAM services by default.
## To disable entirely, set auth_fail_delay to 0.
##
## auth_fail_delay: duration of the delay imposed once the threshold is reached.
## Accepts durations like "2s", "500ms", "1m".
#auth_fail_delay: 2s
## auth_fail_delay_threshold: number of consecutive failures for a single
## user before a delay is imposed on subsequent attempts.
## auth_fail_delay: duration of the delay imposed once the threshold is
## reached. Accepts durations like "2s", "500ms", "1m".
## auth_fail_reset_window: duration of inactivity after the last failure
## before the failure count is automatically reset. Accepts durations
## like "15m", "1h", "30s". Set to 0 to keep failures accumulated
## indefinitely (no inactivity reset).
##
## auth_fail_reset_window: duration of inactivity after the last failure before
## the failure count is automatically reset.
## Accepts durations like "15m", "1h", "30s". Set to 0 to keep failures
## accumulated indefinitely (no inactivity reset).
#auth_fail_reset_window: 15m
## Per-service overrides can be specified under the "services:" key, using
## the PAM service name (e.g. "sshd", "gdm-authd") as the sub-key.
Comment thread
nooreldeenmansour marked this conversation as resolved.
## Only the fields that differ from the default need to be specified.
#pam:
# auth_fail_delay_threshold: 3
# auth_fail_delay: 2s
# auth_fail_reset_window: 15m
# services:
# sshd:
# auth_fail_delay: 5s
19 changes: 15 additions & 4 deletions internal/brokers/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type Manager struct {

transactionsToBroker map[string]*Broker
sessionsToUsername map[string]string
sessionsToServiceName map[string]string
transactionsToBrokerMu sync.RWMutex

cleanup func()
Expand Down Expand Up @@ -111,9 +112,10 @@ func NewManager(ctx context.Context, brokersConfPath string, configuredBrokers [
brokers: brokers,
brokersOrder: brokersOrder,

usersToBroker: make(map[string]*Broker),
transactionsToBroker: make(map[string]*Broker),
sessionsToUsername: make(map[string]string),
usersToBroker: make(map[string]*Broker),
transactionsToBroker: make(map[string]*Broker),
sessionsToUsername: make(map[string]string),
sessionsToServiceName: make(map[string]string),

cleanup: cleanup,
}, nil
Expand Down Expand Up @@ -166,7 +168,7 @@ func (m *Manager) BrokerFromSessionID(id string) (broker *Broker, err error) {
}

// NewSession create a new session for the broker and store the sessionID on the manager.
func (m *Manager) NewSession(brokerID, username, lang, mode, providerID string) (sessionID string, encryptionKey string, err error) {
func (m *Manager) NewSession(brokerID, username, lang, mode, providerID, serviceName string) (sessionID string, encryptionKey string, err error) {
broker, err := m.BrokerFromID(brokerID)
if err != nil {
return "", "", fmt.Errorf("invalid broker: %v", err)
Expand All @@ -183,6 +185,7 @@ func (m *Manager) NewSession(brokerID, username, lang, mode, providerID string)
sessionID, mode, username)
m.transactionsToBroker[sessionID] = broker
m.sessionsToUsername[sessionID] = username
m.sessionsToServiceName[sessionID] = serviceName
return sessionID, encryptionKey, nil
}

Expand All @@ -203,6 +206,7 @@ func (m *Manager) EndSession(sessionID string) error {
sessionID, b.Name)
delete(m.transactionsToBroker, sessionID)
delete(m.sessionsToUsername, sessionID)
delete(m.sessionsToServiceName, sessionID)
m.transactionsToBrokerMu.Unlock()
return nil
}
Expand All @@ -214,6 +218,13 @@ func (m *Manager) UsernameFromSessionID(sessionID string) string {
return m.sessionsToUsername[sessionID]
}

// ServiceNameFromSessionID returns the PAM service name associated with the given session ID.
func (m *Manager) ServiceNameFromSessionID(sessionID string) string {
m.transactionsToBrokerMu.RLock()
defer m.transactionsToBrokerMu.RUnlock()
return m.sessionsToServiceName[sessionID]
}

// BrokerExists returns true if the brokerID is known by the manager.
func (m *Manager) BrokerExists(brokerID string) bool {
_, exists := m.brokers[brokerID]
Expand Down
7 changes: 4 additions & 3 deletions internal/brokers/manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ func TestNewSession(t *testing.T) {
tc.sessionMode = "auth"
}

gotID, gotEKey, err := m.NewSession(tc.brokerID, tc.username, "some_lang", tc.sessionMode, "")
gotID, gotEKey, err := m.NewSession(tc.brokerID, tc.username, "some_lang", tc.sessionMode, "", "sshd")
if tc.wantErr {
require.Error(t, err, "NewSession should return an error, but did not")
return
Expand All @@ -239,6 +239,7 @@ func TestNewSession(t *testing.T) {
gotBroker, err := m.BrokerFromSessionID(gotID)
require.NoError(t, err, "NewSession should have assigned a broker for the session, but did not")
require.Equal(t, wantBroker.ID, gotBroker.ID, "BrokerFromSessionID should have assigned the expected broker for the session, but did not")
require.Equal(t, "sshd", m.ServiceNameFromSessionID(gotID), "NewSession should remember the PAM service name for the session")
})
}
}
Expand Down Expand Up @@ -322,13 +323,13 @@ func TestStartAndEndSession(t *testing.T) {
wg.Add(1)
go func() {
defer wg.Done()
id, key, err := m.NewSession(b1.ID, "user1@example.com", "some_lang", "auth", "")
id, key, err := m.NewSession(b1.ID, "user1@example.com", "some_lang", "auth", "", "sshd")
firstID, firstKey, firstErr = &id, &key, &err
}()
wg.Add(1)
go func() {
defer wg.Done()
id, key, err := m.NewSession(b2.ID, "user2", "some_lang", "auth", "")
id, key, err := m.NewSession(b2.ID, "user2", "some_lang", "auth", "", "gdm-authd")
secondID, secondKey, secondErr = &id, &key, &err
}()
wg.Wait()
Expand Down
13 changes: 11 additions & 2 deletions internal/proto/authd/authd.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions internal/proto/authd/authd.proto
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ message SBRequest {
string username = 2;
string lang = 3;
SessionMode mode = 4;
string service_name = 5;
}

message SBResponse {
Expand Down
22 changes: 12 additions & 10 deletions internal/services/pam/auth_fail_tracker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,33 +10,35 @@ import (
func TestAuthFailTracker_ResetWindow_Zero_DisablesReset(t *testing.T) {
t.Parallel()

tracker := newAuthFailTracker(Config{
cfg := BruteForceMitigationConfig{
AuthFailDelayThreshold: 3,
AuthFailDelay: time.Second,
AuthFailResetWindow: 0,
})
}
tracker := newAuthFailTracker()

// Three consecutive failures should each increment the counter rather than
// resetting it. With the bug (resetWindow == 0 always resets), count would
// stay at 1 on every call.
require.Equal(t, 1, tracker.recordFailure("user"), "first failure")
require.Equal(t, 2, tracker.recordFailure("user"), "second failure")
require.Equal(t, 3, tracker.recordFailure("user"), "third failure: counter must not have been reset")
require.Equal(t, 1, tracker.recordFailure("", "user", cfg.AuthFailResetWindow), "first failure")
require.Equal(t, 2, tracker.recordFailure("", "user", cfg.AuthFailResetWindow), "second failure")
require.Equal(t, 3, tracker.recordFailure("", "user", cfg.AuthFailResetWindow), "third failure: counter must not have been reset")
}

func TestAuthFailTracker_ResetWindow_NonZero_ResetsAfterInactivity(t *testing.T) {
t.Parallel()

tracker := newAuthFailTracker(Config{
cfg := BruteForceMitigationConfig{
AuthFailDelayThreshold: 3,
AuthFailDelay: time.Second,
AuthFailResetWindow: 50 * time.Millisecond,
})
}
tracker := newAuthFailTracker()

require.Equal(t, 1, tracker.recordFailure("user"), "first failure")
require.Equal(t, 2, tracker.recordFailure("user"), "second failure")
require.Equal(t, 1, tracker.recordFailure("", "user", cfg.AuthFailResetWindow), "first failure")
require.Equal(t, 2, tracker.recordFailure("", "user", cfg.AuthFailResetWindow), "second failure")

// After sleeping past the reset window the entry expires and the counter resets.
time.Sleep(100 * time.Millisecond)
require.Equal(t, 1, tracker.recordFailure("user"), "counter should reset after inactivity")
require.Equal(t, 1, tracker.recordFailure("", "user", cfg.AuthFailResetWindow), "counter should reset after inactivity")
}
Loading
Loading