diff --git a/README.md b/README.md index e346dc1..5b4d808 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/pam_ussh.go b/pam_ussh.go index 11ce4d0..3b2ea91 100644 --- a/pam_ussh.go +++ b/pam_ussh.go @@ -34,6 +34,7 @@ import ( "log/syslog" "net" "os" + "os/exec" "path" "runtime" "strings" @@ -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 } @@ -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) @@ -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]) } diff --git a/pam_ussh_test.go b/pam_ussh_test.go index 50decde..331999d 100644 --- a/pam_ussh_test.go +++ b/pam_ussh_test.go @@ -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") @@ -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) @@ -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}) @@ -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) }) }) }