Skip to content

Commit 621ae1b

Browse files
authored
Add /allowlist command (#399)
* move loading whitelist+ops from file to auth and save the loaded files fro reloading * add /whitelist command with lots of open questions * add test for /whitelist * gofmt * use the same auth (the tests don't seem to care, but htis is more right) * mutex whitelistMode and remove some deferred TODOs * s/whitelist/allowlist/ (user-facing); move helper functions outside the handler function * check for ops in Auth.CheckPublicKey and move /allowlist handling to helper functions * possibly fix the test timeout in HostNameCollision * Revert "possibly fix the test timeout in HostNameCollision" (didn't work) This reverts commit 664dbb0. * managed to reproduce the timeout after updating, hopefully it's the same one * remove some unimportant TODOs; add a message when reverify kicks people; add a reverify test * add client connection with key; add test for /allowlist import AGE * hopefully make test less racy * s/whitelist/allowlist/ * fix crash on specifying exactly one more -v flag than the max level * use a key loader function to move file reading out of auth * add loader to allowlist test * minor message changes * add --whitelist with a warning; update tests for messages * apparently, we have another prefix * check names directly on the User objects in TestHostNameCollision * not allowlisted -> not allowed * small message change * update test
1 parent 84bc5c7 commit 621ae1b

File tree

7 files changed

+551
-164
lines changed

7 files changed

+551
-164
lines changed

auth.go

+84-19
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,21 @@ import (
88
"fmt"
99
"net"
1010
"strings"
11+
"sync"
1112
"time"
1213

1314
"github.com/shazow/ssh-chat/set"
1415
"github.com/shazow/ssh-chat/sshd"
1516
"golang.org/x/crypto/ssh"
1617
)
1718

18-
// ErrNotWhitelisted Is the error returned when a key is checked that is not whitelisted,
19-
// when whitelisting is enabled.
20-
var ErrNotWhitelisted = errors.New("not whitelisted")
19+
// KeyLoader loads public keys, e.g. from an authorized_keys file.
20+
// It must return a nil slice on error.
21+
type KeyLoader func() ([]ssh.PublicKey, error)
22+
23+
// ErrNotAllowed Is the error returned when a key is checked that is not allowlisted,
24+
// when allowlisting is enabled.
25+
var ErrNotAllowed = errors.New("not allowed")
2126

2227
// ErrBanned is the error returned when a client is banned.
2328
var ErrBanned = errors.New("banned")
@@ -47,15 +52,20 @@ func newAuthAddr(addr net.Addr) string {
4752
return host
4853
}
4954

50-
// Auth stores lookups for bans, whitelists, and ops. It implements the sshd.Auth interface.
51-
// If the contained passphrase is not empty, it complements a whitelist.
55+
// Auth stores lookups for bans, allowlists, and ops. It implements the sshd.Auth interface.
56+
// If the contained passphrase is not empty, it complements a allowlist.
5257
type Auth struct {
5358
passphraseHash []byte
5459
bannedAddr *set.Set
5560
bannedClient *set.Set
5661
banned *set.Set
57-
whitelist *set.Set
62+
allowlist *set.Set
5863
ops *set.Set
64+
65+
settingsMu sync.RWMutex
66+
allowlistMode bool
67+
opLoader KeyLoader
68+
allowlistLoader KeyLoader
5969
}
6070

6171
// NewAuth creates a new empty Auth.
@@ -64,11 +74,23 @@ func NewAuth() *Auth {
6474
bannedAddr: set.New(),
6575
bannedClient: set.New(),
6676
banned: set.New(),
67-
whitelist: set.New(),
77+
allowlist: set.New(),
6878
ops: set.New(),
6979
}
7080
}
7181

82+
func (a *Auth) AllowlistMode() bool {
83+
a.settingsMu.RLock()
84+
defer a.settingsMu.RUnlock()
85+
return a.allowlistMode
86+
}
87+
88+
func (a *Auth) SetAllowlistMode(value bool) {
89+
a.settingsMu.Lock()
90+
defer a.settingsMu.Unlock()
91+
a.allowlistMode = value
92+
}
93+
7294
// SetPassphrase enables passphrase authentication with the given passphrase.
7395
// If an empty passphrase is given, disable passphrase authentication.
7496
func (a *Auth) SetPassphrase(passphrase string) {
@@ -82,7 +104,7 @@ func (a *Auth) SetPassphrase(passphrase string) {
82104

83105
// AllowAnonymous determines if anonymous users are permitted.
84106
func (a *Auth) AllowAnonymous() bool {
85-
return a.whitelist.Len() == 0 && a.passphraseHash == nil
107+
return !a.AllowlistMode() && a.passphraseHash == nil
86108
}
87109

88110
// AcceptPassphrase determines if passphrase authentication is accepted.
@@ -115,11 +137,11 @@ func (a *Auth) CheckBans(addr net.Addr, key ssh.PublicKey, clientVersion string)
115137
// CheckPubkey determines if a pubkey fingerprint is permitted.
116138
func (a *Auth) CheckPublicKey(key ssh.PublicKey) error {
117139
authkey := newAuthKey(key)
118-
whitelisted := a.whitelist.In(authkey)
119-
if a.AllowAnonymous() || whitelisted {
140+
allowlisted := a.allowlist.In(authkey)
141+
if a.AllowAnonymous() || allowlisted || a.IsOp(key) {
120142
return nil
121143
} else {
122-
return ErrNotWhitelisted
144+
return ErrNotAllowed
123145
}
124146
}
125147

@@ -151,25 +173,68 @@ func (a *Auth) Op(key ssh.PublicKey, d time.Duration) {
151173

152174
// IsOp checks if a public key is an op.
153175
func (a *Auth) IsOp(key ssh.PublicKey) bool {
154-
if key == nil {
155-
return false
156-
}
157176
authkey := newAuthKey(key)
158177
return a.ops.In(authkey)
159178
}
160179

161-
// Whitelist will set a public key as a whitelisted user.
162-
func (a *Auth) Whitelist(key ssh.PublicKey, d time.Duration) {
180+
// LoadOps sets the public keys form loader to operators and saves the loader for later use
181+
func (a *Auth) LoadOps(loader KeyLoader) error {
182+
a.settingsMu.Lock()
183+
a.opLoader = loader
184+
a.settingsMu.Unlock()
185+
return a.ReloadOps()
186+
}
187+
188+
// ReloadOps sets the public keys from a loader saved in the last call to operators
189+
func (a *Auth) ReloadOps() error {
190+
a.settingsMu.RLock()
191+
defer a.settingsMu.RUnlock()
192+
return addFromLoader(a.opLoader, a.Op)
193+
}
194+
195+
// Allowlist will set a public key as a allowlisted user.
196+
func (a *Auth) Allowlist(key ssh.PublicKey, d time.Duration) {
163197
if key == nil {
164198
return
165199
}
200+
var err error
166201
authItem := newAuthItem(key)
167202
if d != 0 {
168-
a.whitelist.Set(set.Expire(authItem, d))
203+
err = a.allowlist.Set(set.Expire(authItem, d))
169204
} else {
170-
a.whitelist.Set(authItem)
205+
err = a.allowlist.Set(authItem)
206+
}
207+
if err == nil {
208+
logger.Debugf("Added to allowlist: %q (for %s)", authItem.Key(), d)
209+
} else {
210+
logger.Errorf("Error adding %q to allowlist for %s: %s", authItem.Key(), d, err)
211+
}
212+
}
213+
214+
// LoadAllowlist adds the public keys from the loader to the allowlist and saves the loader for later use
215+
func (a *Auth) LoadAllowlist(loader KeyLoader) error {
216+
a.settingsMu.Lock()
217+
a.allowlistLoader = loader
218+
a.settingsMu.Unlock()
219+
return a.ReloadAllowlist()
220+
}
221+
222+
// LoadAllowlist adds the public keys from a loader saved in a previous call to the allowlist
223+
func (a *Auth) ReloadAllowlist() error {
224+
a.settingsMu.RLock()
225+
defer a.settingsMu.RUnlock()
226+
return addFromLoader(a.allowlistLoader, a.Allowlist)
227+
}
228+
229+
func addFromLoader(loader KeyLoader, adder func(ssh.PublicKey, time.Duration)) error {
230+
if loader == nil {
231+
return nil
232+
}
233+
keys, err := loader()
234+
for _, key := range keys {
235+
adder(key, 0)
171236
}
172-
logger.Debugf("Added to whitelist: %q (for %s)", authItem.Key(), d)
237+
return err
173238
}
174239

175240
// Ban will set a public key as banned.

auth_test.go

+5-4
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ func ClonePublicKey(key ssh.PublicKey) (ssh.PublicKey, error) {
2121
return ssh.ParsePublicKey(key.Marshal())
2222
}
2323

24-
func TestAuthWhitelist(t *testing.T) {
24+
func TestAuthAllowlist(t *testing.T) {
2525
key, err := NewRandomPublicKey(512)
2626
if err != nil {
2727
t.Fatal(err)
@@ -33,7 +33,8 @@ func TestAuthWhitelist(t *testing.T) {
3333
t.Error("Failed to permit in default state:", err)
3434
}
3535

36-
auth.Whitelist(key, 0)
36+
auth.Allowlist(key, 0)
37+
auth.SetAllowlistMode(true)
3738

3839
keyClone, err := ClonePublicKey(key)
3940
if err != nil {
@@ -46,7 +47,7 @@ func TestAuthWhitelist(t *testing.T) {
4647

4748
err = auth.CheckPublicKey(keyClone)
4849
if err != nil {
49-
t.Error("Failed to permit whitelisted:", err)
50+
t.Error("Failed to permit allowlisted:", err)
5051
}
5152

5253
key2, err := NewRandomPublicKey(512)
@@ -56,7 +57,7 @@ func TestAuthWhitelist(t *testing.T) {
5657

5758
err = auth.CheckPublicKey(key2)
5859
if err == nil {
59-
t.Error("Failed to restrict not whitelisted:", err)
60+
t.Error("Failed to restrict not allowlisted:", err)
6061
}
6162
}
6263

cmd/ssh-chat/cmd.go

+34-40
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,9 @@ type Options struct {
3636
Pprof int `long:"pprof" description:"Enable pprof http server for profiling."`
3737
Verbose []bool `short:"v" long:"verbose" description:"Show verbose logging."`
3838
Version bool `long:"version" description:"Print version and exit."`
39-
Whitelist string `long:"whitelist" description:"Optional file of public keys who are allowed to connect."`
40-
Passphrase string `long:"unsafe-passphrase" description:"Require an interactive passphrase to connect. Whitelist feature is more secure."`
39+
Allowlist string `long:"allowlist" description:"Optional file of public keys who are allowed to connect."`
40+
Whitelist string `long:"whitelist" dexcription:"Old name for allowlist option"`
41+
Passphrase string `long:"unsafe-passphrase" description:"Require an interactive passphrase to connect. Allowlist feature is more secure."`
4142
}
4243

4344
const extraHelp = `There are hidden options and easter eggs in ssh-chat. The source code is a good
@@ -87,7 +88,7 @@ func main() {
8788

8889
// Figure out the log level
8990
numVerbose := len(options.Verbose)
90-
if numVerbose > len(logLevels) {
91+
if numVerbose >= len(logLevels) {
9192
numVerbose = len(logLevels) - 1
9293
}
9394

@@ -141,35 +142,20 @@ func main() {
141142
auth.SetPassphrase(options.Passphrase)
142143
}
143144

144-
err = fromFile(options.Admin, func(line []byte) error {
145-
key, _, _, _, err := ssh.ParseAuthorizedKey(line)
146-
if err != nil {
147-
if err.Error() == "ssh: no key found" {
148-
return nil // Skip line
149-
}
150-
return err
151-
}
152-
auth.Op(key, 0)
153-
return nil
154-
})
145+
err = auth.LoadOps(loaderFromFile(options.Admin, logger))
155146
if err != nil {
156147
fail(5, "Failed to load admins: %v\n", err)
157148
}
158149

159-
err = fromFile(options.Whitelist, func(line []byte) error {
160-
key, _, _, _, err := ssh.ParseAuthorizedKey(line)
161-
if err != nil {
162-
if err.Error() == "ssh: no key found" {
163-
return nil // Skip line
164-
}
165-
return err
166-
}
167-
auth.Whitelist(key, 0)
168-
return nil
169-
})
150+
if options.Allowlist == "" && options.Whitelist != "" {
151+
fmt.Println("--whitelist was renamed to --allowlist.")
152+
options.Allowlist = options.Whitelist
153+
}
154+
err = auth.LoadAllowlist(loaderFromFile(options.Allowlist, logger))
170155
if err != nil {
171-
fail(6, "Failed to load whitelist: %v\n", err)
156+
fail(6, "Failed to load allowlist: %v\n", err)
172157
}
158+
auth.SetAllowlistMode(options.Allowlist != "")
173159

174160
if options.Motd != "" {
175161
host.GetMOTD = func() (string, error) {
@@ -210,24 +196,32 @@ func main() {
210196
fmt.Fprintln(os.Stderr, "Interrupt signal detected, shutting down.")
211197
}
212198

213-
func fromFile(path string, handler func(line []byte) error) error {
199+
func loaderFromFile(path string, logger *golog.Logger) sshchat.KeyLoader {
214200
if path == "" {
215-
// Skip
216201
return nil
217202
}
218-
219-
file, err := os.Open(path)
220-
if err != nil {
221-
return err
222-
}
223-
defer file.Close()
224-
225-
scanner := bufio.NewScanner(file)
226-
for scanner.Scan() {
227-
err := handler(scanner.Bytes())
203+
return func() ([]ssh.PublicKey, error) {
204+
file, err := os.Open(path)
228205
if err != nil {
229-
return err
206+
return nil, err
207+
}
208+
defer file.Close()
209+
210+
var keys []ssh.PublicKey
211+
scanner := bufio.NewScanner(file)
212+
for scanner.Scan() {
213+
key, _, _, _, err := ssh.ParseAuthorizedKey(scanner.Bytes())
214+
if err != nil {
215+
if err.Error() == "ssh: no key found" {
216+
continue // Skip line
217+
}
218+
return nil, err
219+
}
220+
keys = append(keys, key)
221+
}
222+
if keys == nil {
223+
logger.Warning("file", path, "contained no keys")
230224
}
225+
return keys, nil
231226
}
232-
return nil
233227
}

0 commit comments

Comments
 (0)