Index | Thread | Search

From:
David Crumpton <david.m.crumpton@gmail.com>
Subject:
doas -l feature
To:
tech@openbsd.org
Date:
Thu, 5 Jun 2025 17:17:10 -0600

Download raw body.

Thread
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)