diff --git a/GNUmakefile b/GNUmakefile index 2eef88e..423731c 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -24,6 +24,7 @@ install: ${PROG} ${MAN} chmod ${BINMODE} ${DESTDIR}${BINDIR}/${PROG} cp -f doas.1 ${DESTDIR}${MANDIR}/man1 cp -f doas.conf.5 ${DESTDIR}${MANDIR}/man5 + cp -f doas.d.5 ${DESTDIR}${MANDIR}/man5 uninstall: rm -f ${DESTDIR}${BINDIR}/${PROG} diff --git a/README.md b/README.md index f15610c..f967eca 100644 --- a/README.md +++ b/README.md @@ -61,3 +61,12 @@ similar to sudo. See the comment block in `timestamp.c` for an in-depth description on how timestamps are created and checked to be as safe as possible. + +### `--with-doas-confdir` + +An optional feature can be enabled which will result in `doas` reading configuration +snippets from `/etc/doas.d`. These configuration snippets have the same requirements +as `/etc/doas.conf` (owned by root, not world-writable). + +If this feature is enabled, only the `/etc/doas.d` directory is read, and the historical +`/etc/doas.conf` file is ignored. \ No newline at end of file diff --git a/configure b/configure index 1f92f01..a3078dc 100755 --- a/configure +++ b/configure @@ -27,6 +27,7 @@ usage: configure [options] --without-shadow disable shadow support --with-timestamp enable timestamp support + --with-doas-confdir enable configuration directory support --uid-max=NUM set UID_MAX (default 65535) --gid-max=NUM set GID_MAX (default 65535) @@ -38,6 +39,7 @@ EOF # defaults WITHOUT_TIMESTAMP=yes +WITHOUT_CONFDIR=yes UID_MAX=65535 GID_MAX=65535 @@ -56,6 +58,8 @@ for x; do --target) TARGET=$var ;; --enable-debug) DEBUG=yes ;; --enable-static) BUILD_STATIC=yes ;; + --with-doas-confdir) WITHOUT_CONFDIR= ;; + --without-doas-confdir) WITHOUT_CONFDIR=yes ;; --with-pam) WITHOUT_PAM=; WITHOUT_SHADOW=yes ;; --with-shadow) WITHOUT_SHADOW=; WITHOUT_PAM=yes ;; --without-pam) WITHOUT_PAM=yes ;; @@ -558,4 +562,8 @@ fi printf '#define DOAS_CONF "%s/doas.conf"\n' "${SYSCONFDIR}" >>$CONFIG_H +if [ -z "$WITHOUT_CONFDIR" ]; then + printf '#define DOAS_CONFDIR "%s/doas.d"\n' "${SYSCONFDIR}" >>$CONFIG_H +fi + printf '\n#endif /* CONFIG_H */\n' >>$CONFIG_H diff --git a/doas.c b/doas.c index ac3a42a..d77186b 100644 --- a/doas.c +++ b/doas.c @@ -35,6 +35,7 @@ #include #include #include +#include #include "openbsd.h" #include "doas.h" @@ -155,6 +156,7 @@ permit(uid_t uid, gid_t *groups, int ngroups, const struct rule **lastr, static void parseconfig(const char *filename, int checkperms) { + extern const char *yyfn; extern FILE *yyfp; extern int yyparse(void); struct stat sb; @@ -164,6 +166,8 @@ parseconfig(const char *filename, int checkperms) err(1, checkperms ? "doas is not enabled, %s" : "could not open config file %s", filename); + yyfn = filename; + if (checkperms) { if (fstat(fileno(yyfp), &sb) != 0) err(1, "fstat(\"%s\")", filename); @@ -174,11 +178,82 @@ parseconfig(const char *filename, int checkperms) } yyparse(); + yyfn = NULL; + fclose(yyfp); if (parse_errors) exit(1); } +#ifdef DOAS_CONFDIR +static int +isconfdir(const char *dirpath) +{ + struct stat sb; + + if (lstat(dirpath, &sb) != 0) { + if (errno != ENOENT) + err(1, "lstat(\"%s\")", dirpath); + + errno = ENOTDIR; + return 0; + } + + if ((sb.st_mode & (S_IFMT)) == S_IFDIR) + return 1; + + errno = ENOTDIR; + return 0; +} + +static void +parseconfdir(const char *dirpath, int checkperms) +{ + struct dirent **dirent_table; + int i, m, dirent_count; + char pathbuf[PATH_MAX]; + + if (!isconfdir(dirpath)) + err(1, checkperms ? "doas is not enabled, %s" : + "could not open config directory %s", dirpath); + + dirent_count = scandir(dirpath, &dirent_table, NULL, alphasort); + if (dirent_count < 0) + err(1, checkperms ? "doas is not enabled, %s" : + "could not open config directory %s", dirpath); + + for (i = 0, m = 0; i < dirent_count; i++) + { + struct stat sb; + size_t pathlen; + + pathlen = snprintf(pathbuf, sizeof pathbuf, "%s/%s", dirpath, dirent_table[i]->d_name); + free(dirent_table[i]); + + /* make sure path ends in .conf */ + if (pathlen < 6) + continue; + + if (strcmp(pathbuf + (pathlen - 5), ".conf")) + continue; + + if (stat(pathbuf, &sb) != 0) + err(1, "stat(\"%s\")", pathbuf); + + if ((sb.st_mode & (S_IFMT)) != S_IFREG) + continue; + + parseconfig(pathbuf, checkperms); + m++; + } + + free(dirent_table); + + if (!m) + errx(1, "doas is not enabled, %s: no matching configuration files found\n", dirpath); +} +#endif + static void __dead checkconfig(const char *confpath, int argc, char **argv, uid_t uid, gid_t *groups, int ngroups, uid_t target) @@ -188,6 +263,11 @@ checkconfig(const char *confpath, int argc, char **argv, if (setresuid(uid, uid, uid) != 0) err(1, "setresuid"); +#ifdef DOAS_CONFDIR + if (isconfdir(confpath)) + parseconfdir(confpath, 0); + else +#endif parseconfig(confpath, 0); if (!argc) exit(0); @@ -330,6 +410,11 @@ main(int argc, char **argv) if (geteuid()) errx(1, "not installed setuid"); +#ifdef DOAS_CONFDIR + if (isconfdir(DOAS_CONFDIR)) + parseconfdir(DOAS_CONFDIR, 1); + else +#endif parseconfig(DOAS_CONF, 1); /* cmdline is used only for logging, no need to abort on truncate */ diff --git a/doas.conf.5 b/doas.conf.5 index e98bfbe..e90d512 100644 --- a/doas.conf.5 +++ b/doas.conf.5 @@ -143,6 +143,7 @@ permit nopass keepenv setenv { PATH } root as root .Ed .Sh SEE ALSO .Xr doas 1 , +.Xr doas.d 5 , .Xr syslogd 8 .Sh HISTORY The diff --git a/doas.d.5 b/doas.d.5 new file mode 100644 index 0000000..8f5f96a --- /dev/null +++ b/doas.d.5 @@ -0,0 +1,50 @@ +.\"Copyright (c) 2021 Ariadne Conill +.\" +.\"Permission to use, copy, modify, and distribute this software for any +.\"purpose with or without fee is hereby granted, provided that the above +.\"copyright notice and this permission notice appear in all copies. +.\" +.\"THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +.\"WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +.\"MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +.\"ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +.\"WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +.\"ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +.\"OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +.Dd $Mdocdate: October 9 2020 $ +.Dt DOAS.CONF 5 +.Os +.Sh NAME +.Nm doas.conf +.Nd doas configuration file +.Sh DESCRIPTION +The +.Xr doas 1 +utility executes commands as other users according to the rules +configured in either the configuration file or, optionally, the +configuration directory. The preference to use the configuration +file or configuration directory is determined at compile time, +.Xr doas 1 +will only consult one or the other. +.Pp +Configuration snippets stored in the configuration directory +follow the same rules as the classic +.Xr doas 1 +configuration file, documented in +.Xr doas.conf 5 . +They must end with the .conf extension, or they will be ignored. +.Pp +These snippets are read in alphabetical order and thus can be +ordered in the same way as other configuration directories. +.Sh FILES +.Bl -tag -width /etc/doas.d -compact +.It Pa /etc/doas.d +.Xr doas 1 +configuration directory. +.Sh SEE ALSO +.Xr doas 1 , +.Xr doas.conf 5 +.Sh HISTORY +The +.Nm +configuration directory first appeared in OpenDoas. diff --git a/parse.y b/parse.y index 388c2a5..c6d7ebf 100644 --- a/parse.y +++ b/parse.y @@ -49,6 +49,7 @@ typedef struct { } yystype; #define YYSTYPE yystype +const char *yyfn; FILE *yyfp; struct rule **rules; @@ -203,7 +204,7 @@ yyerror(const char *fmt, ...) va_start(va, fmt); vfprintf(stderr, fmt, va); va_end(va); - fprintf(stderr, " at line %d\n", yylval.lineno + 1); + fprintf(stderr, " at %s, line %d\n", yyfn, yylval.lineno + 1); parse_errors++; }