From: David Crumpton Subject: doas -l feature To: tech@openbsd.org Date: Thu, 5 Jun 2025 17:17:10 -0600 I wanted a list option for doas to aid in rule validation for accounts with more rules than the permit wheel rule. I also added a -m option to show what rule matched on success. These options make it easier to debug rules when you have a lot of rules for granular control in addition to auditing user rules. Example output plus cvs diff follow. $ doas -l Commands for user david (1000): Permit group: wsrc (as root) ALL commands [ nopass setenv ] { FTPMODE PKG_CACHE PKG_PATH SM_PATH SSH_AUTH_SOCK DESTDIR DISTDIR FETCH_CMD FLAVOR GROUP MAKE MAKECONF MULTI_PACKAGES NOMAN OKAY_FILES OWNER PKG_DBDIR PKG_DESTDIR PKG_TMPDIR PORTSDIR RELEASEDIR SHARED_ONLY SUBPACKAGE WRKOBJDIR SUDO_PORT_V1 } Permit group: wheel (as root) ALL commands [ keepenv persist ] Permit user: david (as root) command: /usr/sbin/service apache2 restart $ doas -u dcrumpton doas -l doas (david@intbsd.intbsd) password: Commands for user dcrumpton (1003): Permit user: dcrumpton (as root) command: /usr/sbin/rcctl restart nginx $ doas -u dcrumpton doas -l -u operator Commands for user dcrumpton (1003): Deny user: dcrumpton (as operator) command: /usr/sbin/rcctl restart apache2 Permit user: dcrumpton (as operator) command: /usr/local/bin/backup_script [ nopass setenv ] { ZSH2=$ZSH -FOO BAR=high } $ doas -m id Permit group: wheel (as root) ALL commands [ keepenv persist ] uid=0(root) gid=0(wheel) groups=0(wheel), 2(kmem), 3(sys), 4(tty), 5(operator), 20(staff), 31(guest) $ doas -u certbot doas -l No allowed commands for user certbot (1002) $ doas -l ls usage: doas [-Llmns] [-a style] [-C config] [-u user] command [arg ...] $ doas -l -s usage: doas [-Llmns] [-a style] [-C config] [-u user] command [arg ...] Index: doas.1 =================================================================== RCS file: /cvs/src/usr.bin/doas/doas.1,v retrieving revision 1.26 diff -u -p -u -p -r1.26 doas.1 --- doas.1 22 Dec 2022 19:53:22 -0000 1.26 +++ doas.1 5 Jun 2025 23:02:40 -0000 @@ -21,7 +21,7 @@ .Nd execute commands as another user .Sh SYNOPSIS .Nm doas -.Op Fl Lns +.Op Fl Llmns .Op Fl a Ar style .Op Fl C Ar config .Op Fl u Ar user @@ -36,12 +36,15 @@ The argument is mandatory unless .Fl C , .Fl L , +.Fl l , or .Fl s is specified. .Pp The user will be required to authenticate by entering their password, -unless configured otherwise. +unless configured otherwise or when using the +.Fl l +option. .Pp By default, a new environment is created. The variables @@ -97,6 +100,15 @@ No command is executed. Clear any persisted authentications from previous invocations, then immediately exit. No command is executed. +.It Fl l +List allowed commands for the invoking user. The user will not be +challenged for a password. Reveals +.Sq command as +user commands when combined with +.Fl u +option. +.It Fl m +show matched rule .It Fl n Non interactive mode, fail if the matching rule doesn't have the .Ic nopass @@ -126,6 +138,8 @@ The user attempted to run a command whic The password was incorrect. .It The specified command was not found or is not executable. +.It +The list option returned no allowed commands .El .Sh SEE ALSO .Xr su 1 , Index: doas.c =================================================================== RCS file: /cvs/src/usr.bin/doas/doas.c,v retrieving revision 1.99 diff -u -p -u -p -r1.99 doas.c --- doas.c 15 Feb 2024 18:57:58 -0000 1.99 +++ doas.c 5 Jun 2025 23:02:40 -0000 @@ -39,7 +39,7 @@ static void __dead usage(void) { - fprintf(stderr, "usage: doas [-Lns] [-a style] [-C config] [-u user]" + fprintf(stderr, "usage: doas [-Llmns] [-a style] [-C config] [-u user]" " command [arg ...]\n"); exit(1); } @@ -114,10 +114,10 @@ match(uid_t uid, gid_t *groups, int ngro } if (r->target && uidcheck(r->target, target) != 0) return 0; - if (r->cmd) { + if (r->cmd && cmd != NULL) { if (strcmp(r->cmd, cmd)) return 0; - if (r->cmdargs) { + if (r->cmdargs && r->cmdargs != NULL) { /* if arguments were given, they should match explicitly */ for (i = 0; r->cmdargs[i]; i++) { if (!cmdargs[i]) @@ -302,6 +302,102 @@ done: return (unveils); } + +static void +printrule(const struct rule *rule) { + int i; + int group = 0; + + if (rule->ident[0] == ':') + group = 1; + + if(rule->action == PERMIT) + printf(" Permit"); + else + printf(" Deny"); + + if(group) { + printf(" group: %s", rule->ident + 1); + } else { + printf(" user: %s", rule->ident); + } + + if (rule->target) + printf(" (as %s)", rule->target); + else + printf(" (as root)"); + + if (rule->cmd) { + printf(" command: %s", rule->cmd); + if(rule->cmdargs) + for(i = 0; rule->cmdargs[i] != NULL; i++) + printf(" %s", rule->cmdargs[i]); + } else + printf(" ALL commands"); + + if(rule->options || rule->envlist) { + printf(" ["); + + if (rule->options) { + if (rule->options & NOPASS) + printf(" nopass"); + + if (rule->options & KEEPENV) + printf(" keepenv"); + + if (rule->options & PERSIST) + printf(" persist"); + + if (rule->options & NOLOG) + printf(" nolog"); + } + + if (rule->envlist) + printf(" setenv"); + + printf(" ]"); + } + + if(rule->envlist) { + printf(" {"); + for(i = 0; rule->envlist[i] != NULL; i++) + printf(" %s", rule->envlist[i]); + printf(" }"); + } + printf("\n"); +} + +static void __dead +listrules(uid_t uid, gid_t *groups, int ngroups, uid_t target) +{ + int i; + int found = 0; + struct passwd *pw = getpwuid(uid); + char username[LOGIN_NAME_MAX + 1]; + + if (pledge("stdio rpath", NULL) == -1) + err(1, "pledge"); + + strlcpy(username, pw->pw_name, sizeof(username)); + + for (i = 0; i < nrules; i++) { + struct rule *r = rules[i]; + if (match(uid, groups, ngroups, target, NULL, NULL, r)) { + found++; + if(found == 1) + printf("Commands for user %s (%d):\n", username, uid); + + printrule(r); + } + } + + if (!found) { + printf("No allowed commands for user %s (%d)\n", username, uid); + exit(1); + } + exit(0); +} + int main(int argc, char **argv) { @@ -323,6 +419,8 @@ main(int argc, char **argv) int ngroups; int i, ch, rv; int sflag = 0; + int lflag = 0; + int mflag = 0; int nflag = 0; char cwdpath[PATH_MAX]; const char *cwd; @@ -335,7 +433,7 @@ main(int argc, char **argv) uid = getuid(); - while ((ch = getopt(argc, argv, "a:C:Lnsu:")) != -1) { + while ((ch = getopt(argc, argv, "a:C:Llmnsu:")) != -1) { switch (ch) { case 'a': login_style = optarg; @@ -343,6 +441,9 @@ main(int argc, char **argv) case 'C': confpath = optarg; break; + case 'l': + lflag = 1; + break; case 'L': i = open("/dev/tty", O_RDWR); if (i != -1) @@ -352,6 +453,9 @@ main(int argc, char **argv) if (parseuid(optarg, &target) != 0) errx(1, "unknown user"); break; + case 'm': + mflag = 1; + break; case 'n': nflag = 1; break; @@ -369,7 +473,8 @@ main(int argc, char **argv) if (confpath) { if (sflag) usage(); - } else if ((!sflag && !argc) || (sflag && argc)) + } else if ((!sflag && !argc && !lflag) || (sflag && argc) || (lflag && argc) + || (sflag && lflag)) usage(); rv = getpwuid_r(uid, &mypwstore, mypwbuf, sizeof(mypwbuf), &mypw); @@ -382,6 +487,7 @@ main(int argc, char **argv) err(1, "can't get groups"); groups[ngroups++] = getgid(); + if (sflag) { sh = getenv("SHELL"); if (sh == NULL || *sh == '\0') { @@ -405,6 +511,9 @@ main(int argc, char **argv) parseconfig("/etc/doas.conf", 1); + if(lflag) + listrules(uid, groups, ngroups, target); + /* cmdline is used only for logging, no need to abort on truncate */ (void)strlcpy(cmdline, argv[0], sizeof(cmdline)); for (i = 1; i < argc; i++) { @@ -422,6 +531,8 @@ main(int argc, char **argv) "command not permitted for %s: %s", mypw->pw_name, cmdline); errc(1, EPERM, NULL); } + if(mflag) + printrule(rule); if (!(rule->options & NOPASS)) { if (nflag)