add AuthorizedPrincipalsCommand that allows getting authorized_principals
authordjm <djm@openbsd.org>
Thu, 21 May 2015 06:43:30 +0000 (06:43 +0000)
committerdjm <djm@openbsd.org>
Thu, 21 May 2015 06:43:30 +0000 (06:43 +0000)
from a subprocess rather than a file, which is quite useful in
deployments with large userbases

feedback and ok markus@

usr.bin/ssh/auth2-pubkey.c
usr.bin/ssh/servconf.c
usr.bin/ssh/servconf.h
usr.bin/ssh/sshd.c
usr.bin/ssh/sshd_config.5

index 9b5ad55..102fea8 100644 (file)
@@ -1,4 +1,4 @@
-/* $OpenBSD: auth2-pubkey.c,v 1.50 2015/05/21 06:38:35 djm Exp $ */
+/* $OpenBSD: auth2-pubkey.c,v 1.51 2015/05/21 06:43:30 djm Exp $ */
 /*
  * Copyright (c) 2000 Markus Friedl.  All rights reserved.
  *
@@ -551,19 +551,13 @@ match_principals_option(const char *principal_list, struct sshkey_cert *cert)
 }
 
 static int
-match_principals_file(char *file, struct passwd *pw, struct sshkey_cert *cert)
+process_principals(FILE *f, char *file, struct passwd *pw,
+    struct sshkey_cert *cert)
 {
-       FILE *f;
        char line[SSH_MAX_PUBKEY_BYTES], *cp, *ep, *line_opts;
        u_long linenum = 0;
        u_int i;
 
-       temporarily_use_uid(pw);
-       debug("trying authorized principals file %s", file);
-       if ((f = auth_openprincipals(file, pw, options.strict_modes)) == NULL) {
-               restore_uid();
-               return 0;
-       }
        while (read_keyfile_line(f, file, line, sizeof(line), &linenum) != -1) {
                /* Skip leading whitespace. */
                for (cp = line; *cp == ' ' || *cp == '\t'; cp++)
@@ -591,23 +585,127 @@ match_principals_file(char *file, struct passwd *pw, struct sshkey_cert *cert)
                }
                for (i = 0; i < cert->nprincipals; i++) {
                        if (strcmp(cp, cert->principals[i]) == 0) {
-                               debug3("matched principal \"%.100s\" "
-                                   "from file \"%s\" on line %lu",
-                                   cert->principals[i], file, linenum);
+                               debug3("%s:%lu: matched principal \"%.100s\"",
+                                   file == NULL ? "(command)" : file,
+                                   linenum, cert->principals[i]);
                                if (auth_parse_options(pw, line_opts,
                                    file, linenum) != 1)
                                        continue;
-                               fclose(f);
-                               restore_uid();
                                return 1;
                        }
                }
        }
+       return 0;
+}
+
+static int
+match_principals_file(char *file, struct passwd *pw, struct sshkey_cert *cert)
+{
+       FILE *f;
+       int success;
+
+       temporarily_use_uid(pw);
+       debug("trying authorized principals file %s", file);
+       if ((f = auth_openprincipals(file, pw, options.strict_modes)) == NULL) {
+               restore_uid();
+               return 0;
+       }
+       success = process_principals(f, file, pw, cert);
        fclose(f);
        restore_uid();
-       return 0;
+       return success;
 }
 
+/*
+ * Checks whether principal is allowed in output of command.
+ * returns 1 if the principal is allowed or 0 otherwise.
+ */
+static int
+match_principals_command(struct passwd *user_pw, struct sshkey *key)
+{
+       FILE *f = NULL;
+       int ok, found_principal = 0;
+       struct passwd *pw;
+       int i, ac = 0, uid_swapped = 0;
+       pid_t pid;
+       char *tmp, *username = NULL, *command = NULL, **av = NULL;
+       void (*osigchld)(int);
+
+       if (options.authorized_principals_command == NULL)
+               return 0;
+       if (options.authorized_principals_command_user == NULL) {
+               error("No user for AuthorizedPrincipalsCommand specified, "
+                   "skipping");
+               return 0;
+       }
+
+       /*
+        * NB. all returns later this function should go via "out" to
+        * ensure the original SIGCHLD handler is restored properly.
+        */
+       osigchld = signal(SIGCHLD, SIG_DFL);
+
+       /* Prepare and verify the user for the command */
+       username = percent_expand(options.authorized_principals_command_user,
+           "u", user_pw->pw_name, (char *)NULL);
+       pw = getpwnam(username);
+       if (pw == NULL) {
+               error("AuthorizedPrincipalsCommandUser \"%s\" not found: %s",
+                   username, strerror(errno));
+               goto out;
+       }
+
+       /* Turn the command into an argument vector */
+       if (split_argv(options.authorized_principals_command, &ac, &av) != 0) {
+               error("AuthorizedPrincipalsCommand \"%s\" contains "
+                   "invalid quotes", command);
+               goto out;
+       }
+       if (ac == 0) {
+               error("AuthorizedPrincipalsCommand \"%s\" yielded no arguments",
+                   command);
+               goto out;
+       }
+       for (i = 1; i < ac; i++) {
+               tmp = percent_expand(av[i],
+                   "u", user_pw->pw_name,
+                   "h", user_pw->pw_dir,
+                   (char *)NULL);
+               if (tmp == NULL)
+                       fatal("%s: percent_expand failed", __func__);
+               free(av[i]);
+               av[i] = tmp;
+       }
+       /* Prepare a printable command for logs, etc. */
+       command = assemble_argv(ac, av);
+
+       if ((pid = subprocess("AuthorizedPrincipalsCommand", pw, command,
+           ac, av, &f)) == 0)
+               goto out;
+
+       uid_swapped = 1;
+       temporarily_use_uid(pw);
+
+       ok = process_principals(f, NULL, pw, key->cert);
+
+       if (exited_cleanly(pid, "AuthorizedPrincipalsCommand", command) != 0)
+               goto out;
+
+       /* Read completed successfully */
+       found_principal = ok;
+ out:
+       if (f != NULL)
+               fclose(f);
+       signal(SIGCHLD, osigchld);
+       for (i = 0; i < ac; i++)
+               free(av[i]);
+       free(av);
+       if (uid_swapped)
+               restore_uid();
+       free(command);
+       free(username);
+       return found_principal;
+}
 /*
  * Checks whether key is allowed in authorized_keys-format file,
  * returns 1 if the key is allowed or 0 otherwise.
@@ -730,7 +828,7 @@ user_cert_trusted_ca(struct passwd *pw, Key *key)
 {
        char *ca_fp, *principals_file = NULL;
        const char *reason;
-       int ret = 0;
+       int ret = 0, found_principal = 0;
 
        if (!key_is_cert(key) || options.trusted_user_ca_keys == NULL)
                return 0;
@@ -752,14 +850,20 @@ user_cert_trusted_ca(struct passwd *pw, Key *key)
         * against the username.
         */
        if ((principals_file = authorized_principals_file(pw)) != NULL) {
-               if (!match_principals_file(principals_file, pw, key->cert)) {
-                       reason = "Certificate does not contain an "
-                           "authorized principal";
+               if (match_principals_file(principals_file, pw, key->cert))
+                       found_principal = 1;
+       }
+       /* Try querying command if specified */
+       if (!found_principal && match_principals_command(pw, key))
+               found_principal = 1;
+       /* If principals file or command specify, then require a match here */
+       if (!found_principal && (principals_file != NULL ||
+           options.authorized_principals_command != NULL)) {
+               reason = "Certificate does not contain an authorized principal";
  fail_reason:
-                       error("%s", reason);
-                       auth_debug_add("%s", reason);
-                       goto out;
-               }
+               error("%s", reason);
+               auth_debug_add("%s", reason);
+               goto out;
        }
        if (key_cert_check_authority(key, 0, 1,
            principals_file == NULL ? pw->pw_name : NULL, &reason) != 0)
index 0c471d6..d64ba6b 100644 (file)
@@ -1,5 +1,5 @@
 
-/* $OpenBSD: servconf.c,v 1.269 2015/05/04 06:10:48 djm Exp $ */
+/* $OpenBSD: servconf.c,v 1.270 2015/05/21 06:43:30 djm Exp $ */
 /*
  * Copyright (c) 1995 Tatu Ylonen <ylo@cs.hut.fi>, Espoo, Finland
  *                    All rights reserved
@@ -151,6 +151,8 @@ initialize_server_options(ServerOptions *options)
        options->revoked_keys_file = NULL;
        options->trusted_user_ca_keys = NULL;
        options->authorized_principals_file = NULL;
+       options->authorized_principals_command = NULL;
+       options->authorized_principals_command_user = NULL;
        options->ip_qos_interactive = -1;
        options->ip_qos_bulk = -1;
        options->version_addendum = NULL;
@@ -371,6 +373,7 @@ typedef enum {
        sUsePrivilegeSeparation, sAllowAgentForwarding,
        sHostCertificate,
        sRevokedKeys, sTrustedUserCAKeys, sAuthorizedPrincipalsFile,
+       sAuthorizedPrincipalsCommand, sAuthorizedPrincipalsCommandUser,
        sKexAlgorithms, sIPQoS, sVersionAddendum,
        sAuthorizedKeysCommand, sAuthorizedKeysCommandUser,
        sAuthenticationMethods, sHostKeyAgent, sPermitUserRC,
@@ -491,6 +494,8 @@ static struct {
        { "ipqos", sIPQoS, SSHCFG_ALL },
        { "authorizedkeyscommand", sAuthorizedKeysCommand, SSHCFG_ALL },
        { "authorizedkeyscommanduser", sAuthorizedKeysCommandUser, SSHCFG_ALL },
+       { "authorizedprincipalscommand", sAuthorizedPrincipalsCommand, SSHCFG_ALL },
+       { "authorizedprincipalscommanduser", sAuthorizedPrincipalsCommandUser, SSHCFG_ALL },
        { "versionaddendum", sVersionAddendum, SSHCFG_GLOBAL },
        { "authenticationmethods", sAuthenticationMethods, SSHCFG_ALL },
        { "streamlocalbindmask", sStreamLocalBindMask, SSHCFG_ALL },
@@ -1687,6 +1692,34 @@ process_server_config_line(ServerOptions *options, char *line,
                        *charptr = xstrdup(arg);
                break;
 
+       case sAuthorizedPrincipalsCommand:
+               if (cp == NULL)
+                       fatal("%.200s line %d: Missing argument.", filename,
+                           linenum);
+               len = strspn(cp, WHITESPACE);
+               if (*activep &&
+                   options->authorized_principals_command == NULL) {
+                       if (cp[len] != '/' && strcasecmp(cp + len, "none") != 0)
+                               fatal("%.200s line %d: "
+                                   "AuthorizedPrincipalsCommand must be "
+                                   "an absolute path", filename, linenum);
+                       options->authorized_principals_command =
+                           xstrdup(cp + len);
+               }
+               return 0;
+
+       case sAuthorizedPrincipalsCommandUser:
+               charptr = &options->authorized_principals_command_user;
+
+               arg = strdelim(&cp);
+               if (!arg || *arg == '\0')
+                       fatal("%s line %d: missing "
+                           "AuthorizedPrincipalsCommandUser argument.",
+                           filename, linenum);
+               if (*activep && *charptr == NULL)
+                       *charptr = xstrdup(arg);
+               break;
+
        case sAuthenticationMethods:
                if (options->num_auth_methods == 0) {
                        while ((arg = strdelim(&cp)) && *arg != '\0') {
@@ -2177,6 +2210,8 @@ dump_config(ServerOptions *o)
            ? "none" : o->version_addendum);
        dump_cfg_string(sAuthorizedKeysCommand, o->authorized_keys_command);
        dump_cfg_string(sAuthorizedKeysCommandUser, o->authorized_keys_command_user);
+       dump_cfg_string(sAuthorizedPrincipalsCommand, o->authorized_principals_command);
+       dump_cfg_string(sAuthorizedPrincipalsCommandUser, o->authorized_principals_command_user);
        dump_cfg_string(sHostKeyAgent, o->host_key_agent);
        dump_cfg_string(sKexAlgorithms,
            o->kex_algorithms ? o->kex_algorithms : KEX_SERVER_KEX);
index 6886e35..4c4867b 100644 (file)
@@ -1,4 +1,4 @@
-/* $OpenBSD: servconf.h,v 1.117 2015/04/29 03:48:56 dtucker Exp $ */
+/* $OpenBSD: servconf.h,v 1.118 2015/05/21 06:43:31 djm Exp $ */
 
 /*
  * Author: Tatu Ylonen <ylo@cs.hut.fi>
@@ -176,9 +176,11 @@ typedef struct {
        char   *chroot_directory;
        char   *revoked_keys_file;
        char   *trusted_user_ca_keys;
-       char   *authorized_principals_file;
        char   *authorized_keys_command;
        char   *authorized_keys_command_user;
+       char   *authorized_principals_file;
+       char   *authorized_principals_command;
+       char   *authorized_principals_command_user;
 
        int64_t rekey_limit;
        int     rekey_interval;
@@ -214,9 +216,11 @@ struct connection_info {
                M_CP_STROPT(banner); \
                M_CP_STROPT(trusted_user_ca_keys); \
                M_CP_STROPT(revoked_keys_file); \
-               M_CP_STROPT(authorized_principals_file); \
                M_CP_STROPT(authorized_keys_command); \
                M_CP_STROPT(authorized_keys_command_user); \
+               M_CP_STROPT(authorized_principals_file); \
+               M_CP_STROPT(authorized_principals_command); \
+               M_CP_STROPT(authorized_principals_command_user); \
                M_CP_STROPT(hostbased_key_types); \
                M_CP_STROPT(pubkey_key_types); \
                M_CP_STRARRAYOPT(authorized_keys_files, num_authkeys_files); \
index 3cb3da4..d8ea875 100644 (file)
@@ -1,4 +1,4 @@
-/* $OpenBSD: sshd.c,v 1.448 2015/04/27 00:21:21 djm Exp $ */
+/* $OpenBSD: sshd.c,v 1.449 2015/05/21 06:43:31 djm Exp $ */
 /*
  * Author: Tatu Ylonen <ylo@cs.hut.fi>
  * Copyright (c) 1995 Tatu Ylonen <ylo@cs.hut.fi>, Espoo, Finland
@@ -1598,6 +1598,11 @@ main(int ac, char **av)
            strcasecmp(options.authorized_keys_command, "none") != 0))
                fatal("AuthorizedKeysCommand set without "
                    "AuthorizedKeysCommandUser");
+       if (options.authorized_principals_command_user == NULL &&
+           (options.authorized_principals_command != NULL &&
+           strcasecmp(options.authorized_principals_command, "none") != 0))
+               fatal("AuthorizedPrincipalsCommand set without "
+                   "AuthorizedPrincipalsCommandUser");
 
        /*
         * Check whether there is any path through configured auth methods.
index e2e09aa..507a8bf 100644 (file)
@@ -33,7 +33,7 @@
 .\" (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 .\" THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 .\"
-.\" $OpenBSD: sshd_config.5,v 1.201 2015/05/21 06:38:35 djm Exp $
+.\" $OpenBSD: sshd_config.5,v 1.202 2015/05/21 06:43:31 djm Exp $
 .Dd $Mdocdate: May 21 2015 $
 .Dt SSHD_CONFIG 5
 .Os
@@ -287,6 +287,42 @@ directory.
 Multiple files may be listed, separated by whitespace.
 The default is
 .Dq .ssh/authorized_keys .ssh/authorized_keys2 .
+.It Cm AuthorizedPrincipalsCommand
+Specifies a program to be used to generate the list of allowed
+certificate principals as per
+.Cm AuthorizedPrincipalsFile .
+The program must be owned by root, not writable by group or others and
+specified by an absolute path.
+.Pp
+Arguments to
+.Cm AuthorizedPrincipalsCommand
+may be provided using the following tokens, which will be expanded
+at runtime: %% is replaced by a literal '%', %u is replaced by the
+username being authenticated and %h is replaced by the home directory
+of the user being authenticated.
+.Pp
+The program should produce on standard output zero or
+more lines of
+.Cm AuthorizedPrincipalsFile
+output.
+If either
+.Cm AuthorizedPrincipalsCommand
+or
+.Cm AuthorizedPrincipalsFile
+is specified, then certificates offered by the client for authentication
+must contain a principal that is listed.
+By default, no AuthorizedPrincipalsCommand is run.
+.It Cm AuthorizedPrincipalsCommandUser
+Specifies the user under whose account the AuthorizedPrincipalsCommand is run.
+It is recommended to use a dedicated user that has no other role on the host
+than running authorized principals commands.
+If
+.Cm AuthorizedPrincipalsCommand
+is specified but
+.Cm AuthorizedPrincipalsCommandUser
+is not, then
+.Xr sshd 8
+will refuse to start.
 .It Cm AuthorizedPrincipalsFile
 Specifies a file that lists principal names that are accepted for
 certificate authentication.