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
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,13 @@ Runtime configuration options:

* `authorized_principals` - string, comma separated list of authorized principals, default `""`.
If set, the user needs to have a principal in this list in order to use this module. If
this and `authorized_principals_file` are both set, only the last option listed is checked.
this and `authorized_principals_file` or `authorized_principals_command` are set, only the last option listed is checked.

* `authorized_principals_file` - string, path to an authorized_principals file, default `""`.
If set, users need to have a principal listed in this file in order to use this module.
If this and `authorized_principals` are both set, only the last option listed is checked.
If this and `authorized_principals` or `authorized_principals_command` are set, only the last option listed is checked.

* `authorized_principals_command` - string, absolute path to a program that generates a list of valid principals. If set, users need to have a principal generated by this program in order to use this module. If this and `authorized_principals` or `authorized_principals_file` are set, only the last option is checked.

* `group` - string, default, `""`
If set, the user needs to be a member of this group in order to use this module.
Expand Down
32 changes: 31 additions & 1 deletion pam_ussh.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import (
"log/syslog"
"net"
"os"
"os/exec"
"path"
"runtime"
"strings"
Expand Down Expand Up @@ -155,7 +156,16 @@ func authenticate(w io.Writer, uid int, username, ca string, principals map[stri
continue
}

if err := c.CheckCert(username, cert); err != nil {
// If a manual set of principals is provided, don't require
// the username to be in the certificate's principals
// Principals are verified at the end of this function
testedPrincipal := username
if len(principals) > 0 && len(cert.ValidPrincipals) > 0 {
testedPrincipal = cert.ValidPrincipals[0]
}

if err := c.CheckCert(testedPrincipal, cert); err != nil {
pamLog("Error validating cert: %v\n", err)
continue
}

Expand Down Expand Up @@ -216,6 +226,19 @@ func loadValidPrincipals(principals string) (map[string]struct{}, error) {
return p, nil
}

func executePrincipalsCommand(command string) (map[string]struct{}, error) {
args := strings.Split(command, " ")
out, err := exec.Command(args[0], args[1:]...).Output()
if err != nil {
return nil, err
}
p := make(map[string]struct{})
for _, principal := range strings.Split(strings.TrimSpace(string(out)), "\n") {
p[principal] = struct{}{}
}
return p, nil
}

func pamAuthenticate(w io.Writer, uid int, username string, argv []string) AuthResult {
runtime.GOMAXPROCS(1)

Expand Down Expand Up @@ -243,6 +266,13 @@ func pamAuthenticate(w io.Writer, uid int, username string, argv []string) AuthR
return AuthError
}
authorizedPrincipals = ap
case "authorized_principals_command":
ap, err := executePrincipalsCommand(opt[1])
if err != nil {
pamLog("%v", err)
return AuthError
}
authorizedPrincipals = ap
default:
pamLog("unkown option: %s\n", opt[0])
}
Expand Down
33 changes: 33 additions & 0 deletions pam_ussh_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,18 @@ func TestLoadPrincipals(t *testing.T) {
require.True(t, ok)
})
}
func TestExecutePrincipalCommand(t *testing.T) {
WithTempDir(func(dir string) {
p := path.Join(dir, "script")
e := ioutil.WriteFile(p, []byte("#!/bin/bash\necho \"test\""), 0755)
require.NoError(t, e)

r, e := executePrincipalsCommand(p)
require.NoError(t, e)
_, ok := r["test"]
require.True(t, ok)
})
}

func TestNoAuthSock(t *testing.T) {
oldAgent := os.Getenv("SSH_AUTH_SOCK")
Expand Down Expand Up @@ -109,6 +121,7 @@ func TestPamAuthorize(t *testing.T) {
ca := path.Join(dir, "ca")
caPamOpt := fmt.Sprintf("ca_file=%s", ca)
principals := path.Join(dir, "principals")
principalsCommand := path.Join(dir, "principalsCommand")

k, e := rsa.GenerateKey(rand.Reader, 1024)
require.NoError(t, e)
Expand All @@ -125,6 +138,9 @@ func TestPamAuthorize(t *testing.T) {
e = ioutil.WriteFile(principals, []byte("group:foober"), 0444)
require.NoError(t, e)

e = ioutil.WriteFile(principalsCommand, []byte("#!/bin/bash\necho 'foober'"), 0755)
require.NoError(t, e)

WithSSHAgent(func(a agent.Agent) {
a.Add(agent.AddedKey{PrivateKey: userPriv, Certificate: c})

Expand Down Expand Up @@ -156,6 +172,23 @@ func TestPamAuthorize(t *testing.T) {
r = pamAuthenticate(new(bytes.Buffer), getUID(), "foober", []string{caPamOpt,
"group=nosuchgroup"})
require.Equal(t, AuthSuccess, r)

// positive test with authorized_principals_command pam option
r = pamAuthenticate(new(bytes.Buffer), getUID(), "foober", []string{caPamOpt,
fmt.Sprintf("authorized_principals_command=%s", principalsCommand)})
require.Equal(t, AuthSuccess, r)

// negative test with authorized_principals_command pam option
e = ioutil.WriteFile(principalsCommand, []byte("#!/bin/bash\necho 'duber'"), 0555)
require.NoError(t, e)
r = pamAuthenticate(new(bytes.Buffer), getUID(), "foober", []string{caPamOpt,
fmt.Sprintf("authorized_principals_command=%s", principalsCommand)})
require.Equal(t, AuthError, r)

// negative test with bad authorized_principals_command pam option
r = pamAuthenticate(new(bytes.Buffer), getUID(), "foober", []string{caPamOpt,
"authorized_principals_command=foober"})
require.Equal(t, AuthError, r)
})
})
}
Expand Down