diff options
author | Lukas Fleischer <lfleischer@archlinux.org> | 2015-04-11 13:25:59 +0200 |
---|---|---|
committer | Lukas Fleischer <lfleischer@archlinux.org> | 2015-04-11 14:08:30 +0200 |
commit | ef1f3798a0d06fa5e3ba9ae9cda0d1000e4cc57b (patch) | |
tree | 7d0b8fe5ee9534c82e2a0a80067ae61115acc2c3 /scripts | |
parent | 4f4cfff620ecaa27e4b50f542f6f1e9af9d08e30 (diff) | |
download | aurweb-ef1f3798a0d06fa5e3ba9ae9cda0d1000e4cc57b.tar.xz |
Update the OpenSSH patch
Use the latest version of Damien Miller's patch to extend the parameters
to the AuthorizedKeysCommand.
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
Diffstat (limited to 'scripts')
-rw-r--r-- | scripts/git-integration/0001-Patch-sshd-for-the-AUR.patch | 1146 | ||||
-rwxr-xr-x | scripts/git-integration/git-auth.py | 14 | ||||
-rw-r--r-- | scripts/git-integration/sshd_config | 2 |
3 files changed, 1053 insertions, 109 deletions
diff --git a/scripts/git-integration/0001-Patch-sshd-for-the-AUR.patch b/scripts/git-integration/0001-Patch-sshd-for-the-AUR.patch index 6b72712..688b115 100644 --- a/scripts/git-integration/0001-Patch-sshd-for-the-AUR.patch +++ b/scripts/git-integration/0001-Patch-sshd-for-the-AUR.patch @@ -1,11 +1,10 @@ -From e23745b61a46f034bca3cab9936c24c249afdc7f Mon Sep 17 00:00:00 2001 -From: Lukas Fleischer <archlinux@cryptocrack.de> -Date: Sun, 21 Dec 2014 22:17:48 +0100 +From 6423ae83d38535687d52097b7854b3c81151fe34 Mon Sep 17 00:00:00 2001 +From: Lukas Fleischer <lfleischer@archlinux.org> +Date: Sat, 11 Apr 2015 12:57:46 +0200 Subject: [PATCH] Patch sshd for the AUR -* Add SSH_KEY_FINGERPRINT and SSH_KEY variables to the environment of - the AuthorizedKeysCommand which allows for efficiently looking up SSH - keys in the AUR database. +* Apply the latest version of Damien Miller's patch to extend the + parameters to the AuthorizedKeysCommand. * Remove the secure path check for the AuthorizedKeysCommand. We are running the sshd under a non-privileged user who has as little @@ -14,139 +13,1082 @@ Subject: [PATCH] Patch sshd for the AUR * Prevent from running the sshd as root. -Signed-off-by: Lukas Fleischer <archlinux@cryptocrack.de> +Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org> --- - auth2-pubkey.c | 48 +++++++++++++++++++++++++++++++++++++++++++----- - ssh.h | 12 ++++++++++++ - sshd.c | 5 +++++ - sshd_config.5 | 5 +++++ - 4 files changed, 65 insertions(+), 5 deletions(-) + auth2-pubkey.c | 530 +++++++++++++++++++++++++++++++++++++++++++-------------- + servconf.c | 35 ++++ + servconf.h | 8 +- + ssh.c | 5 + + sshd.c | 5 + + sshd_config.5 | 54 +++++- + sshkey.c | 172 +++++++++++-------- + sshkey.h | 1 + + 8 files changed, 606 insertions(+), 204 deletions(-) diff --git a/auth2-pubkey.c b/auth2-pubkey.c -index 0a3c1de..baf4922 100644 +index d943efa..2ce0a4b 100644 --- a/auth2-pubkey.c +++ b/auth2-pubkey.c -@@ -510,6 +510,8 @@ user_key_command_allowed2(struct passwd *user_pw, Key *key) - int status, devnull, p[2], i; - pid_t pid; - char *username, errmsg[512]; -+ struct sshbuf *b = NULL, *bb = NULL; -+ char *keytext, *uu = NULL; - - if (options.authorized_keys_command == NULL || - options.authorized_keys_command[0] != '/') -@@ -538,11 +540,6 @@ user_key_command_allowed2(struct passwd *user_pw, Key *key) - options.authorized_keys_command, strerror(errno)); - goto out; - } -- if (auth_secure_path(options.authorized_keys_command, &st, NULL, 0, -- errmsg, sizeof(errmsg)) != 0) { -- error("Unsafe AuthorizedKeysCommand: %s", errmsg); -- goto out; -- } +@@ -65,6 +65,9 @@ + #include "monitor_wrap.h" + #include "authfile.h" + #include "match.h" ++#include "ssherr.h" ++#include "channels.h" /* XXX for session.h */ ++#include "session.h" /* XXX for child_set_env(); refactor? */ - if (pipe(p) != 0) { - error("%s: pipe: %s", __func__, strerror(errno)); -@@ -568,6 +565,47 @@ user_key_command_allowed2(struct passwd *user_pw, Key *key) - for (i = 0; i < NSIG; i++) - signal(i, SIG_DFL); + /* import */ + extern ServerOptions options; +@@ -248,6 +251,227 @@ pubkey_auth_info(Authctxt *authctxt, const Key *key, const char *fmt, ...) + free(extra); + } -+ keytext = key_fingerprint(key, SSH_FP_MD5, SSH_FP_HEX); -+ if (setenv(SSH_KEY_FINGERPRINT_ENV_NAME, keytext, 1) == -1) { -+ error("%s: setenv: %s", __func__, strerror(errno)); -+ _exit(1); -+ } ++/* ++ * Splits 's' into an argument vector. Handles quoted string and basic ++ * escape characters (\\, \", \'). Caller must free the argument vector ++ * and its members. ++ */ ++static int ++split_argv(const char *s, int *argcp, char ***argvp) ++{ ++ int r = SSH_ERR_INTERNAL_ERROR; ++ int argc = 0, quote, i, j; ++ char *arg, **argv = xcalloc(1, sizeof(*argv)); + -+ if (!(b = sshbuf_new()) || !(bb = sshbuf_new())) { -+ error("%s: sshbuf_new: %s", __func__, strerror(errno)); -+ _exit(1); ++ *argvp = NULL; ++ *argcp = 0; ++ ++ for (i = 0; s[i] != '\0'; i++) { ++ /* Skip leading whitespace */ ++ if (s[i] == ' ' || s[i] == '\t') ++ continue; ++ ++ /* Start of a token */ ++ quote = 0; ++ if (s[i] == '\\' && ++ (s[i + 1] == '\'' || s[i + 1] == '\"' || s[i + 1] == '\\')) ++ i++; ++ else if (s[i] == '\'' || s[i] == '"') ++ quote = s[i++]; ++ ++ argv = xrealloc(argv, (argc + 2), sizeof(*argv)); ++ arg = argv[argc++] = xcalloc(1, strlen(s + i) + 1); ++ argv[argc] = NULL; ++ ++ /* Copy the token in, removing escapes */ ++ for (j = 0; s[i] != '\0'; i++) { ++ if (s[i] == '\\') { ++ if (s[i + 1] == '\'' || ++ s[i + 1] == '\"' || ++ s[i + 1] == '\\') { ++ i++; /* Skip '\' */ ++ arg[j++] = s[i]; ++ } else { ++ /* Unrecognised escape */ ++ arg[j++] = s[i]; ++ } ++ } else if (quote == 0 && (s[i] == ' ' || s[i] == '\t')) ++ break; /* done */ ++ else if (quote != 0 && s[i] == quote) ++ break; /* done */ ++ else ++ arg[j++] = s[i]; ++ } ++ if (s[i] == '\0') { ++ if (quote != 0) { ++ /* Ran out of string looking for close quote */ ++ r = SSH_ERR_INVALID_FORMAT; ++ goto out; ++ } ++ break; + } -+ if (sshkey_to_blob_buf(key, bb) != 0) { -+ error("%s: sshkey_to_blob_buf: %s", __func__, ++ } ++ /* Success */ ++ *argcp = argc; ++ *argvp = argv; ++ argc = 0; ++ argv = NULL; ++ r = 0; ++ out: ++ if (argc != 0 && argv != NULL) { ++ for (i = 0; i < argc; i++) ++ free(argv[i]); ++ free(argv); ++ } ++ return r; ++} ++ ++/* ++ * Runs command in a subprocess. Returns pid on success and a FILE* to the ++ * subprocess' stdout or 0 on failure. ++ * NB. "command" is only used for logging. ++ */ ++static pid_t ++subprocess(const char *tag, struct passwd *pw, const char *command, ++ int ac, char **av, FILE **child) ++{ ++ FILE *f; ++ struct stat st; ++ int devnull, p[2], i; ++ pid_t pid; ++ char *cp, errmsg[512]; ++ u_int envsize; ++ char **child_env; ++ ++ *child = NULL; ++ ++ debug3("%s: %s command \"%s\" running as %s", __func__, ++ tag, command, pw->pw_name); ++ ++ /* Verify the path exists and is safe-ish to execute */ ++ if (*av[0] != '/') { ++ error("%s path is not absolute", tag); ++ return 0; ++ } ++ temporarily_use_uid(pw); ++ if (stat(av[0], &st) < 0) { ++ error("Could not stat %s \"%s\": %s", tag, ++ av[0], strerror(errno)); ++ restore_uid(); ++ return 0; ++ } ++ ++ /* ++ * Run the command; stderr is left in place, stdout is the ++ * authorized_keys output. ++ */ ++ if (pipe(p) != 0) { ++ error("%s: pipe: %s", tag, strerror(errno)); ++ restore_uid(); ++ return 0; ++ } ++ ++ /* ++ * Don't want to call this in the child, where it can fatal() and ++ * run cleanup_exit() code. ++ */ ++ restore_uid(); ++ ++ switch ((pid = fork())) { ++ case -1: /* error */ ++ error("%s: fork: %s", tag, strerror(errno)); ++ close(p[0]); ++ close(p[1]); ++ return 0; ++ case 0: /* child */ ++ /* Prepare a minimal environment for the child. */ ++ envsize = 5; ++ child_env = xcalloc(sizeof(*child_env), envsize); ++ child_set_env(&child_env, &envsize, "PATH", _PATH_STDPATH); ++ child_set_env(&child_env, &envsize, "USER", pw->pw_name); ++ child_set_env(&child_env, &envsize, "LOGNAME", pw->pw_name); ++ child_set_env(&child_env, &envsize, "HOME", pw->pw_dir); ++ if ((cp = getenv("LANG")) != NULL) ++ child_set_env(&child_env, &envsize, "LANG", cp); ++ ++ for (i = 0; i < NSIG; i++) ++ signal(i, SIG_DFL); ++ ++ if ((devnull = open(_PATH_DEVNULL, O_RDWR)) == -1) { ++ error("%s: open %s: %s", tag, _PATH_DEVNULL, + strerror(errno)); + _exit(1); + } -+ if (!(uu = sshbuf_dtob64(bb))) { -+ error("%s: sshbuf_dtob64: %s", __func__, -+ strerror(errno)); ++ /* Keep stderr around a while longer to catch errors */ ++ if (dup2(devnull, STDIN_FILENO) == -1 || ++ dup2(p[1], STDOUT_FILENO) == -1) { ++ error("%s: dup2: %s", tag, strerror(errno)); + _exit(1); + } -+ if (sshbuf_putf(b, "%s ", sshkey_ssh_name(key))) { -+ error("%s: sshbuf_putf: %s", __func__, ++ closefrom(STDERR_FILENO + 1); ++ ++ /* Don't use permanently_set_uid() here to avoid fatal() */ ++ if (setresgid(pw->pw_gid, pw->pw_gid, pw->pw_gid) != 0) { ++ error("%s: setresgid %u: %s", tag, (u_int)pw->pw_gid, + strerror(errno)); + _exit(1); + } -+ if (sshbuf_put(b, uu, strlen(uu) + 1)) { -+ error("%s: sshbuf_put: %s", __func__, ++ if (setresuid(pw->pw_uid, pw->pw_uid, pw->pw_uid) != 0) { ++ error("%s: setresuid %u: %s", tag, (u_int)pw->pw_uid, + strerror(errno)); + _exit(1); + } -+ if (setenv(SSH_KEY_ENV_NAME, sshbuf_ptr(b), 1) == -1) { -+ error("%s: setenv: %s", __func__, strerror(errno)); ++ /* stdin is pointed to /dev/null at this point */ ++ if (dup2(STDIN_FILENO, STDERR_FILENO) == -1) { ++ error("%s: dup2: %s", tag, strerror(errno)); + _exit(1); + } -+ if (uu) -+ free(uu); -+ if (b) -+ sshbuf_free(b); -+ if (bb) -+ sshbuf_free(bb); -+ - if ((devnull = open(_PATH_DEVNULL, O_RDWR)) == -1) { - error("%s: open %s: %s", __func__, _PATH_DEVNULL, - strerror(errno)); -diff --git a/ssh.h b/ssh.h -index c94633b..411ea86 100644 ---- a/ssh.h -+++ b/ssh.h -@@ -97,3 +97,15 @@ - - /* Listen backlog for sshd, ssh-agent and forwarding sockets */ - #define SSH_LISTEN_BACKLOG 128 + -+/* -+ * Name of the environment variable containing the incoming key passed -+ * to AuthorizedKeysCommand. ++ execve(av[0], av, child_env); ++ error("%s exec \"%s\": %s", tag, command, strerror(errno)); ++ _exit(127); ++ default: /* parent */ ++ break; ++ } ++ ++ close(p[1]); ++ if ((f = fdopen(p[0], "r")) == NULL) { ++ error("%s: fdopen: %s", tag, strerror(errno)); ++ close(p[0]); ++ /* Don't leave zombie child */ ++ kill(pid, SIGTERM); ++ while (waitpid(pid, NULL, 0) == -1 && errno == EINTR) ++ ; ++ return 0; ++ } ++ /* Success */ ++ debug3("%s: %s pid %ld", __func__, tag, (long)pid); ++ *child = f; ++ return pid; ++} ++ ++/* Returns 0 if pid exited cleanly, non-zero otherwise */ ++static int ++exited_cleanly(pid_t pid, const char *tag, const char *cmd) ++{ ++ int status; ++ ++ while (waitpid(pid, &status, 0) == -1) { ++ if (errno != EINTR) { ++ error("%s: waitpid: %s", tag, strerror(errno)); ++ return -1; ++ } ++ } ++ if (WIFSIGNALED(status)) { ++ error("%s %s exited on signal %d", tag, cmd, WTERMSIG(status)); ++ return -1; ++ } else if (WEXITSTATUS(status) != 0) { ++ error("%s %s failed, status %d", tag, cmd, WEXITSTATUS(status)); ++ return -1; ++ } ++ return 0; ++} ++ + static int + match_principals_option(const char *principal_list, struct sshkey_cert *cert) + { +@@ -269,19 +493,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++) +@@ -309,24 +527,119 @@ 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. + */ -+#define SSH_KEY_ENV_NAME "SSH_KEY" ++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 *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; ++ } ++ ++ command = percent_expand(options.authorized_principals_command, ++ "u", user_pw->pw_name, "h", user_pw->pw_dir, (char *)NULL); ++ ++ /* Turn the command into an argument vector */ ++ if (split_argv(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; ++ } ++ ++ 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)) ++ 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; ++} +/* -+ * Name of the environment variable containing the incoming key fingerprint -+ * passed to AuthorizedKeysCommand. -+ */ -+#define SSH_KEY_FINGERPRINT_ENV_NAME "SSH_KEY_FINGERPRINT" -diff --git a/sshd.c b/sshd.c -index 4e01855..60c676f 100644 ---- a/sshd.c -+++ b/sshd.c -@@ -1424,6 +1424,11 @@ main(int ac, char **av) - av = saved_argv; - #endif + * Checks whether key is allowed in authorized_keys-format file, + * returns 1 if the key is allowed or 0 otherwise. + */ +@@ -448,7 +761,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; +@@ -470,14 +783,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) +@@ -526,144 +845,105 @@ user_key_allowed2(struct passwd *pw, Key *key, char *file) + static int + user_key_command_allowed2(struct passwd *user_pw, Key *key) + { +- FILE *f; +- int ok, found_key = 0; ++ FILE *f = NULL; ++ int r, ok, found_key = 0; + struct passwd *pw; +- struct stat st; +- int status, devnull, p[2], i; ++ int i, uid_swapped = 0, ac = 0; + pid_t pid; +- char *username, errmsg[512]; ++ char *username = NULL, *key_fp = NULL, *keytext = NULL; ++ char *command = NULL, **av = NULL; ++ void (*osigchld)(int); + +- if (options.authorized_keys_command == NULL || +- options.authorized_keys_command[0] != '/') ++ if (options.authorized_keys_command == NULL) + return 0; +- + if (options.authorized_keys_command_user == NULL) { + error("No user for AuthorizedKeysCommand 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_keys_command_user, + "u", user_pw->pw_name, (char *)NULL); + pw = getpwnam(username); + if (pw == NULL) { + error("AuthorizedKeysCommandUser \"%s\" not found: %s", + username, strerror(errno)); +- free(username); +- return 0; ++ goto out; + } +- free(username); +- +- temporarily_use_uid(pw); -+ if (geteuid() == 0) { +- if (stat(options.authorized_keys_command, &st) < 0) { +- error("Could not stat AuthorizedKeysCommand \"%s\": %s", +- options.authorized_keys_command, strerror(errno)); ++ /* Prepare AuthorizedKeysCommand */ ++ if ((key_fp = sshkey_fingerprint(key, options.fingerprint_hash, ++ SSH_FP_DEFAULT)) == NULL) { ++ error("%s: sshkey_fingerprint failed", __func__); + goto out; + } +- if (auth_secure_path(options.authorized_keys_command, &st, NULL, 0, +- errmsg, sizeof(errmsg)) != 0) { +- error("Unsafe AuthorizedKeysCommand: %s", errmsg); ++ if ((r = sshkey_to_base64(key, &keytext)) != 0) { ++ error("%s: sshkey_to_base64 failed: %s", __func__, ssh_err(r)); + goto out; + } +- +- if (pipe(p) != 0) { +- error("%s: pipe: %s", __func__, strerror(errno)); ++ command = percent_expand(options.authorized_keys_command, ++ "u", user_pw->pw_name, "h", user_pw->pw_dir, ++ "t", sshkey_ssh_name(key), "f", key_fp, "k", keytext, (char *)NULL); ++ ++ /* Turn the command into an argument vector */ ++ if (split_argv(command, &ac, &av) != 0) { ++ error("AuthorizedKeysCommand \"%s\" contains invalid quotes", ++ command); ++ goto out; ++ } ++ if (ac == 0) { ++ error("AuthorizedKeysCommand \"%s\" yielded no arguments", ++ command); + goto out; + } +- +- debug3("Running AuthorizedKeysCommand: \"%s %s\" as \"%s\"", +- options.authorized_keys_command, user_pw->pw_name, pw->pw_name); + + /* +- * Don't want to call this in the child, where it can fatal() and +- * run cleanup_exit() code. ++ * If AuthorizedKeysCommand was run without arguments ++ * then fall back to the old behaviour of passing the ++ * target username as a single argument. + */ +- restore_uid(); +- +- switch ((pid = fork())) { +- case -1: /* error */ +- error("%s: fork: %s", __func__, strerror(errno)); +- close(p[0]); +- close(p[1]); +- return 0; +- case 0: /* child */ +- for (i = 0; i < NSIG; i++) +- signal(i, SIG_DFL); +- +- if ((devnull = open(_PATH_DEVNULL, O_RDWR)) == -1) { +- error("%s: open %s: %s", __func__, _PATH_DEVNULL, +- strerror(errno)); +- _exit(1); +- } +- /* Keep stderr around a while longer to catch errors */ +- if (dup2(devnull, STDIN_FILENO) == -1 || +- dup2(p[1], STDOUT_FILENO) == -1) { +- error("%s: dup2: %s", __func__, strerror(errno)); +- _exit(1); +- } +- closefrom(STDERR_FILENO + 1); +- +- /* Don't use permanently_set_uid() here to avoid fatal() */ +- if (setresgid(pw->pw_gid, pw->pw_gid, pw->pw_gid) != 0) { +- error("setresgid %u: %s", (u_int)pw->pw_gid, +- strerror(errno)); +- _exit(1); +- } +- if (setresuid(pw->pw_uid, pw->pw_uid, pw->pw_uid) != 0) { +- error("setresuid %u: %s", (u_int)pw->pw_uid, +- strerror(errno)); +- _exit(1); +- } +- /* stdin is pointed to /dev/null at this point */ +- if (dup2(STDIN_FILENO, STDERR_FILENO) == -1) { +- error("%s: dup2: %s", __func__, strerror(errno)); +- _exit(1); +- } +- +- execl(options.authorized_keys_command, +- options.authorized_keys_command, user_pw->pw_name, NULL); +- +- error("AuthorizedKeysCommand %s exec failed: %s", +- options.authorized_keys_command, strerror(errno)); +- _exit(127); +- default: /* parent */ +- break; ++ if (ac == 1) { ++ av = xrealloc(av, ac + 2, sizeof(*av)); ++ av[1] = xstrdup(user_pw->pw_name); ++ av[2] = NULL; ++ /* Fix up command too, since it is used in log messages */ ++ free(command); ++ xasprintf(&command, "%s %s", av[0], av[1]); + } + ++ if ((pid = subprocess("AuthorizedKeysCommand", pw, command, ++ ac, av, &f)) == 0) ++ goto out; ++ ++ uid_swapped = 1; + temporarily_use_uid(pw); + +- close(p[1]); +- if ((f = fdopen(p[0], "r")) == NULL) { +- error("%s: fdopen: %s", __func__, strerror(errno)); +- close(p[0]); +- /* Don't leave zombie child */ +- kill(pid, SIGTERM); +- while (waitpid(pid, NULL, 0) == -1 && errno == EINTR) +- ; +- goto out; +- } + ok = check_authkeys_file(f, options.authorized_keys_command, key, pw); +- fclose(f); + +- while (waitpid(pid, &status, 0) == -1) { +- if (errno != EINTR) { +- error("%s: waitpid: %s", __func__, strerror(errno)); +- goto out; +- } +- } +- if (WIFSIGNALED(status)) { +- error("AuthorizedKeysCommand %s exited on signal %d", +- options.authorized_keys_command, WTERMSIG(status)); ++ if (exited_cleanly(pid, "AuthorizedKeysCommand", command)) + goto out; +- } else if (WEXITSTATUS(status) != 0) { +- error("AuthorizedKeysCommand %s returned status %d", +- options.authorized_keys_command, WEXITSTATUS(status)); +- goto out; +- } ++ ++ /* Read completed successfully */ + found_key = ok; + out: +- restore_uid(); ++ 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); ++ free(key_fp); ++ free(keytext); + return found_key; + } + +diff --git a/servconf.c b/servconf.c +index 3185462..510cdde 100644 +--- a/servconf.c ++++ b/servconf.c +@@ -159,6 +159,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; +@@ -396,6 +398,7 @@ typedef enum { + sUsePrivilegeSeparation, sAllowAgentForwarding, + sHostCertificate, + sRevokedKeys, sTrustedUserCAKeys, sAuthorizedPrincipalsFile, ++ sAuthorizedPrincipalsCommand, sAuthorizedPrincipalsCommandUser, + sKexAlgorithms, sIPQoS, sVersionAddendum, + sAuthorizedKeysCommand, sAuthorizedKeysCommandUser, + sAuthenticationMethods, sHostKeyAgent, sPermitUserRC, +@@ -528,6 +531,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 }, +@@ -1697,6 +1702,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 (*activep && options->num_auth_methods == 0) { + while ((arg = strdelim(&cp)) && *arg != '\0') { +@@ -2166,6 +2199,8 @@ dump_config(ServerOptions *o) + dump_cfg_string(sVersionAddendum, 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); +diff --git a/servconf.h b/servconf.h +index 9922f0c..35d6673 100644 +--- a/servconf.h ++++ b/servconf.h +@@ -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); \ +diff --git a/ssh.c b/ssh.c +index 0ad82f0..abf4e54 100644 +--- a/ssh.c ++++ b/ssh.c +@@ -548,6 +548,11 @@ main(int ac, char **av) + original_real_uid = getuid(); + original_effective_uid = geteuid(); + ++ if (original_effective_uid == 0) { + fprintf(stderr, "this is a patched version of the sshd that must not be run as root.\n"); + exit(1); + } + - if (geteuid() == 0 && setgroups(0, NULL) == -1) - debug("setgroups(): %.200s", strerror(errno)); + /* + * Use uid-swapping to give up root privileges for the duration of + * option processing. We will re-instantiate the rights when we are +diff --git a/sshd.c b/sshd.c +index 6aa17fa..672c486 100644 +--- a/sshd.c ++++ b/sshd.c +@@ -1694,6 +1694,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. diff --git a/sshd_config.5 b/sshd_config.5 -index ef36d33..1d7bade 100644 +index 6dce0c7..a267af9 100644 --- a/sshd_config.5 +++ b/sshd_config.5 -@@ -223,6 +223,11 @@ It will be invoked with a single argument of the username - being authenticated, and should produce on standard output zero or +@@ -230,9 +230,21 @@ The default is not to require multiple authentication; successful completion + of a single authentication method is sufficient. + .It Cm AuthorizedKeysCommand + Specifies a program to be used to look up the user's public keys. +-The program must be owned by root and not writable by group or others. +-It will be invoked with a single argument of the username +-being authenticated, and should produce on standard output zero or ++The program must be owned by root, not writable by group or others and ++specified by an absolute path. ++.Pp ++Arguments to ++.Cm AuthorizedKeysCommand ++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, %h is replaced by the home directory ++of the user being authenticated, %t is replaced with the key type ++offered for authentication, %f is replaced with the fingerprint of ++the key, and %k is replaced with the key being offered for authentication. ++If no arguments are specified then the username of the target user ++will be supplied. ++.Pp ++The program should produce on standard output zero or more lines of authorized_keys output (see AUTHORIZED_KEYS in .Xr sshd 8 ) . -+The key being used for authentication (the key's type and the key text itself, -+separated by a space) will be available in the -+.Ev SSH_KEY -+environment variable, and the fingerprint of the key will be available in the -+.Ev SSH_KEY_FINGERPRINT environment variable. If a key supplied by AuthorizedKeysCommand does not successfully authenticate - and authorize the user then public key authentication continues using the usual - .Cm AuthorizedKeysFile +@@ -271,6 +283,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. +diff --git a/sshkey.c b/sshkey.c +index 3cc3f44..ecb61fd 100644 +--- a/sshkey.c ++++ b/sshkey.c +@@ -761,6 +761,12 @@ to_blob_buf(const struct sshkey *key, struct sshbuf *b, int force_plain) + if (key == NULL) + return SSH_ERR_INVALID_ARGUMENT; + ++ if (sshkey_is_cert(key)) { ++ if (key->cert == NULL) ++ return SSH_ERR_EXPECTED_CERT; ++ if (sshbuf_len(key->cert->certblob) == 0) ++ return SSH_ERR_KEY_LACKS_CERTBLOB; ++ } + type = force_plain ? sshkey_type_plain(key->type) : key->type; + typename = sshkey_ssh_name_from_type_nid(type, key->ecdsa_nid); + +@@ -1409,98 +1415,116 @@ sshkey_read(struct sshkey *ret, char **cpp) + } + + int +-sshkey_write(const struct sshkey *key, FILE *f) ++sshkey_to_base64(const struct sshkey *key, char **b64p) + { +- int ret = SSH_ERR_INTERNAL_ERROR; +- struct sshbuf *b = NULL, *bb = NULL; ++ int r = SSH_ERR_INTERNAL_ERROR; ++ struct sshbuf *b = NULL; + char *uu = NULL; ++ ++ if (b64p != NULL) ++ *b64p = NULL; ++ if ((b = sshbuf_new()) == NULL) ++ return SSH_ERR_ALLOC_FAIL; ++ if ((r = sshkey_putb(key, b)) != 0) ++ goto out; ++ if ((uu = sshbuf_dtob64(b)) == NULL) { ++ r = SSH_ERR_ALLOC_FAIL; ++ goto out; ++ } ++ /* Success */ ++ if (b64p != NULL) { ++ *b64p = uu; ++ uu = NULL; ++ } ++ r = 0; ++ out: ++ sshbuf_free(b); ++ free(uu); ++ return r; ++} ++ ++static int ++sshkey_format_rsa1(const struct sshkey *key, struct sshbuf *b) ++{ ++ int r = SSH_ERR_INTERNAL_ERROR; + #ifdef WITH_SSH1 + u_int bits = 0; + char *dec_e = NULL, *dec_n = NULL; +-#endif /* WITH_SSH1 */ + +- if (sshkey_is_cert(key)) { +- if (key->cert == NULL) +- return SSH_ERR_EXPECTED_CERT; +- if (sshbuf_len(key->cert->certblob) == 0) +- return SSH_ERR_KEY_LACKS_CERTBLOB; ++ if (key->rsa == NULL || key->rsa->e == NULL || ++ key->rsa->n == NULL) { ++ r = SSH_ERR_INVALID_ARGUMENT; ++ goto out; + } +- if ((b = sshbuf_new()) == NULL) +- return SSH_ERR_ALLOC_FAIL; +- switch (key->type) { +-#ifdef WITH_SSH1 +- case KEY_RSA1: +- if (key->rsa == NULL || key->rsa->e == NULL || +- key->rsa->n == NULL) { +- ret = SSH_ERR_INVALID_ARGUMENT; +- goto out; +- } +- if ((dec_e = BN_bn2dec(key->rsa->e)) == NULL || +- (dec_n = BN_bn2dec(key->rsa->n)) == NULL) { +- ret = SSH_ERR_ALLOC_FAIL; +- goto out; +- } +- /* size of modulus 'n' */ +- if ((bits = BN_num_bits(key->rsa->n)) <= 0) { +- ret = SSH_ERR_INVALID_ARGUMENT; +- goto out; +- } +- if ((ret = sshbuf_putf(b, "%u %s %s", bits, dec_e, dec_n)) != 0) +- goto out; ++ if ((dec_e = BN_bn2dec(key->rsa->e)) == NULL || ++ (dec_n = BN_bn2dec(key->rsa->n)) == NULL) { ++ r = SSH_ERR_ALLOC_FAIL; ++ goto out; ++ } ++ /* size of modulus 'n' */ ++ if ((bits = BN_num_bits(key->rsa->n)) <= 0) { ++ r = SSH_ERR_INVALID_ARGUMENT; ++ goto out; ++ } ++ if ((r = sshbuf_putf(b, "%u %s %s", bits, dec_e, dec_n)) != 0) ++ goto out; ++ ++ /* Success */ ++ r = 0; ++ out: ++ if (dec_e != NULL) ++ OPENSSL_free(dec_e); ++ if (dec_n != NULL) ++ OPENSSL_free(dec_n); + #endif /* WITH_SSH1 */ +- break; +-#ifdef WITH_OPENSSL +- case KEY_DSA: +- case KEY_DSA_CERT_V00: +- case KEY_DSA_CERT: +- case KEY_ECDSA: +- case KEY_ECDSA_CERT: +- case KEY_RSA: +- case KEY_RSA_CERT_V00: +- case KEY_RSA_CERT: +-#endif /* WITH_OPENSSL */ +- case KEY_ED25519: +- case KEY_ED25519_CERT: +- if ((bb = sshbuf_new()) == NULL) { +- ret = SSH_ERR_ALLOC_FAIL; +- goto out; +- } +- if ((ret = sshkey_putb(key, bb)) != 0) +- goto out; +- if ((uu = sshbuf_dtob64(bb)) == NULL) { +- ret = SSH_ERR_ALLOC_FAIL; ++ ++ return r; ++} ++ ++static int ++sshkey_format_text(const struct sshkey *key, struct sshbuf *b) ++{ ++ int r = SSH_ERR_INTERNAL_ERROR; ++ char *uu = NULL; ++ ++ if (key->type == KEY_RSA1) { ++ if ((r = sshkey_format_rsa1(key, b)) != 0) + goto out; +- } +- if ((ret = sshbuf_putf(b, "%s ", sshkey_ssh_name(key))) != 0) ++ } else { ++ /* Unsupported key types handled in sshkey_to_base64() */ ++ if ((r = sshkey_to_base64(key, &uu)) != 0) + goto out; +- if ((ret = sshbuf_put(b, uu, strlen(uu))) != 0) ++ if ((r = sshbuf_putf(b, "%s %s", ++ sshkey_ssh_name(key), uu)) != 0) + goto out; +- break; +- default: +- ret = SSH_ERR_KEY_TYPE_UNKNOWN; +- goto out; + } ++ r = 0; ++ out: ++ free(uu); ++ return r; ++} ++ ++int ++sshkey_write(const struct sshkey *key, FILE *f) ++{ ++ struct sshbuf *b = NULL; ++ int r = SSH_ERR_INTERNAL_ERROR; ++ ++ if ((b = sshbuf_new()) == NULL) ++ return SSH_ERR_ALLOC_FAIL; ++ if ((r = sshkey_format_text(key, b)) != 0) ++ goto out; + if (fwrite(sshbuf_ptr(b), sshbuf_len(b), 1, f) != 1) { + if (feof(f)) + errno = EPIPE; +- ret = SSH_ERR_SYSTEM_ERROR; ++ r = SSH_ERR_SYSTEM_ERROR; + goto out; + } +- ret = 0; ++ /* Success */ ++ r = 0; + out: +- if (b != NULL) +- sshbuf_free(b); +- if (bb != NULL) +- sshbuf_free(bb); +- if (uu != NULL) +- free(uu); +-#ifdef WITH_SSH1 +- if (dec_e != NULL) +- OPENSSL_free(dec_e); +- if (dec_n != NULL) +- OPENSSL_free(dec_n); +-#endif /* WITH_SSH1 */ +- return ret; ++ sshbuf_free(b); ++ return r; + } + + const char * +diff --git a/sshkey.h b/sshkey.h +index 62c1c3e..98f1ca9 100644 +--- a/sshkey.h ++++ b/sshkey.h +@@ -163,6 +163,7 @@ int sshkey_from_blob(const u_char *, size_t, struct sshkey **); + int sshkey_fromb(struct sshbuf *, struct sshkey **); + int sshkey_froms(struct sshbuf *, struct sshkey **); + int sshkey_to_blob(const struct sshkey *, u_char **, size_t *); ++int sshkey_to_base64(const struct sshkey *, char **); + int sshkey_putb(const struct sshkey *, struct sshbuf *); + int sshkey_puts(const struct sshkey *, struct sshbuf *); + int sshkey_plain_to_blob(const struct sshkey *, u_char **, size_t *); -- -2.2.1 +2.3.5 diff --git a/scripts/git-integration/git-auth.py b/scripts/git-integration/git-auth.py index 801a1d3..09dadec 100755 --- a/scripts/git-integration/git-auth.py +++ b/scripts/git-integration/git-auth.py @@ -4,6 +4,7 @@ import configparser import mysql.connector import os import re +import sys config = configparser.RawConfigParser() config.read(os.path.dirname(os.path.realpath(__file__)) + "/../../conf/config") @@ -14,14 +15,14 @@ aur_db_user = config.get('database', 'user') aur_db_pass = config.get('database', 'password') aur_db_socket = config.get('database', 'socket') -key_prefixes = config.get('auth', 'key-prefixes').split() +valid_keytypes = config.get('auth', 'valid-keytypes').split() username_regex = config.get('auth', 'username-regex') git_serve_cmd = config.get('auth', 'git-serve-cmd') ssh_opts = config.get('auth', 'ssh-options') -pubkey = os.environ.get("SSH_KEY") -valid_prefixes = tuple(p + " " for p in key_prefixes) -if pubkey is None or not pubkey.startswith(valid_prefixes): +keytype = sys.argv[1] +keytext = sys.argv[2] +if not keytype in valid_keytypes: exit(1) db = mysql.connector.connect(host=aur_db_host, user=aur_db_user, @@ -30,7 +31,7 @@ db = mysql.connector.connect(host=aur_db_host, user=aur_db_user, cur = db.cursor() cur.execute("SELECT Username FROM Users WHERE SSHPubKey = %s " + - "AND Suspended = 0", (pubkey,)) + "AND Suspended = 0", (keytype + " " + keytext,)) if cur.rowcount != 1: exit(1) @@ -39,4 +40,5 @@ user = cur.fetchone()[0] if not re.match(username_regex, user): exit(1) -print('command="%s %s",%s %s' % (git_serve_cmd, user, ssh_opts, pubkey)) +print('command="%s %s",%s %s' % (git_serve_cmd, user, ssh_opts, + keytype + " " + keytext)) diff --git a/scripts/git-integration/sshd_config b/scripts/git-integration/sshd_config index 64a45f8..d3d2f5a 100644 --- a/scripts/git-integration/sshd_config +++ b/scripts/git-integration/sshd_config @@ -2,5 +2,5 @@ Port 2222 HostKey ~/.ssh/ssh_host_rsa_key PasswordAuthentication no UsePrivilegeSeparation no -AuthorizedKeysCommand /srv/http/aurweb/scripts/git-integration/git-auth.py +AuthorizedKeysCommand /srv/http/aurweb/scripts/git-integration/git-auth.py "%t" "%k" AuthorizedKeysCommandUser aur |