Download raw body.
doas -l feature
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)
doas -l feature