diff --git a/INSTALL.md b/INSTALL.md index 73d19368d..e6a123138 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -375,6 +375,10 @@ Defaults are listed in brackets after the description. ldap_sasl_interactive_bind_s() function is present in the LDAP libraries. + --with-apparmor + Enable support for the AppArmor Linux Security Module (LSM) on + supported systems. + --with-logincap This adds support for login classes specified in `/etc/login.conf`. It is enabled by default on BSD/OS, Darwin, FreeBSD, OpenBSD, and diff --git a/MANIFEST b/MANIFEST index ccd3aa01e..2fb53d929 100644 --- a/MANIFEST +++ b/MANIFEST @@ -1178,6 +1178,7 @@ scripts/mkpkg scripts/pp scripts/unanon src/Makefile.in +src/apparmor.c src/conversation.c src/copy_file.c src/edit_open.c diff --git a/config.h.in b/config.h.in index ac94b4983..d0be95822 100644 --- a/config.h.in +++ b/config.h.in @@ -51,6 +51,9 @@ /* Define to 1 if you use AIX general authentication. */ #undef HAVE_AIXAUTH +/* Define to 1 to enable AppArmor support. */ +#undef HAVE_APPARMOR + /* Define to 1 if you have the `arc4random' function. */ #undef HAVE_ARC4RANDOM diff --git a/configure.ac b/configure.ac index 541f62066..71325fb5a 100644 --- a/configure.ac +++ b/configure.ac @@ -67,6 +67,7 @@ AC_SUBST([BAMAN]) AC_SUBST([LCMAN]) AC_SUBST([PSMAN]) AC_SUBST([SEMAN]) +AC_SUBST([AAMAN]) AC_SUBST([devdir]) AC_SUBST([mansectsu]) AC_SUBST([mansectform]) @@ -251,6 +252,7 @@ BAMAN=0 LCMAN=0 PSMAN=0 SEMAN=0 +AAMAN=0 LIBINTL= LIBCRYPTO= LIBMD= @@ -1483,6 +1485,19 @@ AC_ARG_WITH(selinux, [AS_HELP_STRING([--with-selinux], [enable SELinux support]) ;; esac], [with_selinux=no]) +AC_ARG_WITH(apparmor, [AS_HELP_STRING([--with-apparmor], [enable AppArmor support])], +[case $with_apparmor in + yes) AC_DEFINE(HAVE_APPARMOR) + AAMAN=1 + SUDO_OBJS="${SUDO_OBJS} apparmor.o" + AC_CHECK_LIB(apparmor, aa_change_profile, + [SUDO_LIBS="${SUDO_LIBS} -lapparmor"]) + ;; + no) ;; + *) AC_MSG_ERROR([--with-apparmor does not take an argument.]) + +esac], [with_apparmor=no]) + AC_ARG_ENABLE(sasl, [AS_HELP_STRING([--enable-sasl], [Enable/disable LDAP SASL support])], [ case "$enableval" in @@ -5416,6 +5431,7 @@ AH_TEMPLATE(HAVE_PROJECT_H, [Define to 1 if you have the header file AH_TEMPLATE(HAVE_SECURID, [Define to 1 if you use SecurID for authentication.]) AH_TEMPLATE(HAVE_SELINUX, [Define to 1 to enable SELinux RBAC support.]) AH_TEMPLATE(HAVE_SETKEYCREATECON, [Define to 1 if you have the `setkeycreatecon' function.]) +AH_TEMPLATE(HAVE_APPARMOR, [Define to 1 to enable AppArmor support.]) AH_TEMPLATE(HAVE_SHL_LOAD, [Define to 1 if you have the `shl_load' function.]) AH_TEMPLATE(HAVE_SKEY, [Define to 1 if you use S/Key.]) AH_TEMPLATE(HAVE_SKEYACCESS, [Define to 1 if your S/Key library has skeyaccess().]) diff --git a/docker/debian/latest/Dockerfile b/docker/debian/latest/Dockerfile index 9e2d5b9a0..80417ffd8 100644 --- a/docker/debian/latest/Dockerfile +++ b/docker/debian/latest/Dockerfile @@ -5,5 +5,6 @@ RUN DEBIAN_FRONTEND=noninteractive TZ=America/Denver apt-get update && \ build-essential curl dpkg-dev ed libldap2-dev libpam0g-dev \ libsasl2-dev libselinux1-dev libsepol1-dev libssl-dev zlib1g-dev \ libaudit-dev libssl-dev python3-dev libpython3-dev libwolfssl-dev \ + libapparmor-dev \ file lsb-release fakeroot pkg-config procps git ssh openssh-client RUN useradd -ms /bin/bash build diff --git a/docker/debian/testing/Dockerfile b/docker/debian/testing/Dockerfile index f8498bb20..34aa72d23 100644 --- a/docker/debian/testing/Dockerfile +++ b/docker/debian/testing/Dockerfile @@ -5,5 +5,6 @@ RUN DEBIAN_FRONTEND=noninteractive TZ=America/Denver apt-get update && \ build-essential curl dpkg-dev ed libldap2-dev libpam0g-dev \ libsasl2-dev libselinux1-dev libsepol1-dev libssl-dev zlib1g-dev \ libaudit-dev libssl-dev python3-dev libpython3-dev libwolfssl-dev \ + libapparmor-dev \ file lsb-release fakeroot pkg-config procps git ssh openssh-client RUN useradd -ms /bin/bash build diff --git a/docker/ubuntu/devel/Dockerfile b/docker/ubuntu/devel/Dockerfile index 5ded20e3b..fffc141b5 100644 --- a/docker/ubuntu/devel/Dockerfile +++ b/docker/ubuntu/devel/Dockerfile @@ -5,5 +5,6 @@ RUN DEBIAN_FRONTEND=noninteractive TZ=America/Denver apt-get update && \ build-essential curl dpkg-dev ed libldap2-dev libpam0g-dev \ libsasl2-dev libselinux1-dev libsepol1-dev libssl-dev zlib1g-dev \ libaudit-dev libssl-dev python3-dev libpython3-dev libwolfssl-dev \ + libapparmor-dev \ file lsb-release fakeroot pkg-config procps git ssh openssh-client RUN useradd -ms /bin/bash build diff --git a/docker/ubuntu/latest/Dockerfile b/docker/ubuntu/latest/Dockerfile index db619f97e..a566c5df9 100644 --- a/docker/ubuntu/latest/Dockerfile +++ b/docker/ubuntu/latest/Dockerfile @@ -5,5 +5,6 @@ RUN DEBIAN_FRONTEND=noninteractive TZ=America/Denver apt-get update && \ build-essential curl dpkg-dev ed libldap2-dev libpam0g-dev \ libsasl2-dev libselinux1-dev libsepol1-dev libssl-dev zlib1g-dev \ libaudit-dev libssl-dev python3-dev libpython3-dev libwolfssl-dev \ + libapparmor-dev \ file lsb-release fakeroot pkg-config procps git ssh openssh-client RUN useradd -ms /bin/bash build diff --git a/docker/ubuntu/rolling/Dockerfile b/docker/ubuntu/rolling/Dockerfile index 6a9aebfc0..2d6a0981d 100644 --- a/docker/ubuntu/rolling/Dockerfile +++ b/docker/ubuntu/rolling/Dockerfile @@ -5,5 +5,6 @@ RUN DEBIAN_FRONTEND=noninteractive TZ=America/Denver apt-get update && \ build-essential curl dpkg-dev ed libldap2-dev libpam0g-dev \ libsasl2-dev libselinux1-dev libsepol1-dev libssl-dev zlib1g-dev \ libaudit-dev libssl-dev python3-dev libpython3-dev libwolfssl-dev \ + libapparmor-dev \ file lsb-release fakeroot pkg-config procps git ssh openssh-client RUN useradd -ms /bin/bash build diff --git a/docs/sudoers.man.in b/docs/sudoers.man.in index 636cd9303..ca4b96d3c 100644 --- a/docs/sudoers.man.in +++ b/docs/sudoers.man.in @@ -1290,6 +1290,8 @@ Runas_Spec ::= '(' Runas_List? (':' Runas_List)? ')' SELinux_Spec ::= ('ROLE=role' | 'TYPE=type') .\} +AppArmor_Spec ::= 'APPARMOR_PROFILE=profile' + .if \n(PS \{\ Solaris_Priv_Spec ::= ('PRIVS=privset' | 'LIMITPRIVS=privset') @@ -1503,6 +1505,7 @@ Options may consist of .if \n(SL \{\ SELinux roles and/or types, .\} +AppArmor profiles, .if \n(PS \{\ Solaris privileges sets, .\} @@ -1533,6 +1536,59 @@ A role or type specified on the command line, however, will supersede the values in \fIsudoers\fR. .\} +.SS "AppArmor_Spec" +On systems supporting AppArmor, +\fIsudoers\fR +file entries may optionally specify an AppArmor profile that should be +used to confine a command. +If an AppArmor profile is specified with the command, it will override +any default values specified in +\fIsudoers\fR. +Appropriate profile transition rules must be defined to support the +profile change specified for a user. +.PP +AppArmor profiles can be specified in any way that complies with the +rules of +aa_change_profile(2). +For instance, in the following +\fIsudoers\fR +entry +.nf +.sp +.RS 0n +alice ALL = (root) APPARMOR_PROFILE=my-profile ALL +.RE +.fi +.PP +the user +\fBalice\fR +may run any command as root under confinement by the profile +\(oqmy-profile\(cq. +You can also stack profiles, or allow a user to run commands unconfined by +any profile. E.g., +.nf +.sp +.RS 0n +bob ALL = (root) APPARMOR_PROFILE=foo//&bar /usr/bin/vi +cathy ALL = (root) APPARMOR_PROFILE=unconfined /bin/ls +.RE +.fi +.PP +These +\fIsudoers\fR +entries allow user +\fBbob\fR +to run +\fI/usr/bin/vi\fR +as root under the stacked profiles +\(oqfoo\(cq +and +\(oqbar\(cq, +and user +\fBcathy\fR +to run +\fI/bin/ls\fR +without any confinement at all. .if \n(PS \{\ .SS "Solaris_Priv_Spec" On Solaris systems, @@ -4161,6 +4217,19 @@ which does not create a new PAM session. .PP \fBStrings\fR: .TP 18n +apparmor_profile +The default AppArmor profile to transition into when executing the +command. +The default +\fIapparmor_profile\fR +can be overriden for individual +\fIsudoers\fR +entries by specifying the +\fIAPPARMOR_PROFILE\fR +option. +This option is only available when sudo is built with AppArmor +support. +.TP 18n authfail_message Message that is displayed after a user fails to authenticate. The message may include the diff --git a/docs/sudoers.mdoc.in b/docs/sudoers.mdoc.in index 5e2fe5717..7809db02f 100644 --- a/docs/sudoers.mdoc.in +++ b/docs/sudoers.mdoc.in @@ -21,6 +21,7 @@ .\" Materiel Command, USAF, under agreement number F39502-99-1-0512. .\" .nr SL @SEMAN@ +.nr AA @AAMAN@ .nr BA @BAMAN@ .nr LC @LCMAN@ .nr PS @PSMAN@ @@ -1231,13 +1232,23 @@ Runas_Spec ::= '(' Runas_List? (':' Runas_List)? ')' .el Option_Spec ::= (SELinux_Spec | Date_Spec | Timeout_Spec | Chdir_Spec | Chroot_Spec) .\} .el \{\ +.ie \n(AA \{\ +.ie \n(PS Option_Spec ::= (AppArmor_Spec | Solaris_Priv_Spec | Date_Spec | Timeout_Spec | Chdir_Spec | Chroot_Spec) +.el Option_Spec ::= (AppArmor_Spec | Date_Spec | Timeout_Spec | Chdir_Spec | Chroot_Spec) +.\} +.el \{\ .ie \n(PS Option_Spec ::= (Solaris_Priv_Spec | Date_Spec | Timeout_Spec | Chdir_Spec | Chroot_Spec) .el Option_Spec ::= (Date_Spec | Timeout_Spec | Chdir_Spec | Chroot_Spec) .\} +.\} .if \n(SL \{\ SELinux_Spec ::= ('ROLE=role' | 'TYPE=type') +.\} +.if \n(AA \{\ +AppArmor_Spec ::= 'APPARMOR_PROFILE=profile' + .\} .if \n(PS \{\ Solaris_Priv_Spec ::= ('PRIVS=privset' | 'LIMITPRIVS=privset') @@ -1427,6 +1438,9 @@ Options may consist of .if \n(SL \{\ SELinux roles and/or types, .\} +.if \n(AA \{\ +AppArmor profiles, +.\} .if \n(PS \{\ Solaris privileges sets, .\} @@ -1457,6 +1471,55 @@ A role or type specified on the command line, however, will supersede the values in .Em sudoers . .\} +.if \n(AA \{\ +.Ss AppArmor_Spec +On systems supporting AppArmor, +.Em sudoers +file entries may optionally specify an AppArmor profile that should be +used to confine a command. +If an AppArmor profile is specified with the command, it will override +any default values specified in +.Em sudoers . +Appropriate profile transition rules must be defined to support the +profile change specified for a user. +.Pp +AppArmor profiles can be specified in any way that complies with the +rules of +.Xr aa_change_profile 2 . +For instance, in the following +.Em sudoers +entry +.Bd -literal +alice ALL = (root) APPARMOR_PROFILE=my-profile ALL +.Ed +.Pp +the user +.Sy alice +may run any command as root under confinement by the profile +.Ql my-profile . +You can also stack profiles, or allow a user to run commands unconfined by +any profile. E.g., +.Bd -literal +bob ALL = (root) APPARMOR_PROFILE=foo//&bar /usr/bin/vi +cathy ALL = (root) APPARMOR_PROFILE=unconfined /bin/ls +.Ed +.Pp +These +.Em sudoers +entries allow user +.Sy bob +to run +.Pa /usr/bin/vi +as root under the stacked profiles +.Ql foo +and +.Ql bar , +and user +.Sy cathy +to run +.Pa /bin/ls +without any confinement at all. +.\} .if \n(PS \{\ .Ss Solaris_Priv_Spec On Solaris systems, @@ -3931,6 +3994,20 @@ which does not create a new PAM session. .Pp .Sy Strings : .Bl -tag -width 16n +.if \n(AA \{\ +.It apparmor_profile +The default AppArmor profile to transition into when executing the +command. +The default +.Em apparmor_profile +can be overriden for individual +.Em sudoers +entries by specifying the +.Em APPARMOR_PROFILE +option. +This option is only available when sudo is built with AppArmor +support. +.\} .It authfail_message Message that is displayed after a user fails to authenticate. The message may include the diff --git a/include/sudo_debug.h b/include/sudo_debug.h index b598aefd0..399602b8f 100644 --- a/include/sudo_debug.h +++ b/include/sudo_debug.h @@ -85,6 +85,7 @@ struct sudo_conf_debug_file_list; #define SUDO_DEBUG_SELINUX (12<<6) /* selinux */ #define SUDO_DEBUG_UTIL (13<<6) /* utility functions */ #define SUDO_DEBUG_UTMP (14<<6) /* utmp file ops */ +#define SUDO_DEBUG_APPARMOR (15<<6) /* AppArmor */ #define SUDO_DEBUG_ALL 0xffff0000 /* all subsystems */ /* Error return for sudo_debug_register(). */ diff --git a/plugins/sudoers/check.c b/plugins/sudoers/check.c index 21dc2e4da..6f5d9cc0a 100644 --- a/plugins/sudoers/check.c +++ b/plugins/sudoers/check.c @@ -190,6 +190,9 @@ check_user(int validated, int mode) #ifdef HAVE_SELINUX if (user_role == NULL && user_type == NULL) #endif +#ifdef HAVE_APPARMOR + if (user_apparmor_profile == NULL) +#endif #ifdef HAVE_PRIV_SET if (runas_privs == NULL && runas_limitprivs == NULL) #endif diff --git a/plugins/sudoers/cvtsudoers_csv.c b/plugins/sudoers/cvtsudoers_csv.c index bcfa28d9f..b13861d63 100644 --- a/plugins/sudoers/cvtsudoers_csv.c +++ b/plugins/sudoers/cvtsudoers_csv.c @@ -553,6 +553,14 @@ print_cmndspec_csv(FILE *fp, struct sudoers_parse_tree *parse_tree, } #endif /* HAVE_SELINUX */ +#ifdef HAVE_APPARMOR + if (cs->apparmor_profile != NULL) { + fprintf(fp, "%sapparmor_profile=%s,", need_comma ? "," : "", + cs->apparmor_profile); + need_comma = true; + } +#endif /* HAVE_APPARMOR */ + #ifdef HAVE_PRIV_SET /* Print Solaris privs/limitprivs */ if (cs->privs != NULL || cs->limitprivs != NULL) { @@ -571,7 +579,7 @@ print_cmndspec_csv(FILE *fp, struct sudoers_parse_tree *parse_tree, /* * Merge adjacent commands with matching tags, runas, SELinux - * role/type and Solaris priv settings. + * role/type, AppArmor profiles and Solaris priv settings. */ for (;;) { /* Does the next entry differ only in the command itself? */ @@ -585,6 +593,9 @@ print_cmndspec_csv(FILE *fp, struct sudoers_parse_tree *parse_tree, #ifdef HAVE_SELINUX || cs->role != next->role || cs->type != next->type #endif /* HAVE_SELINUX */ +#ifdef HAVE_APPARMOR + || cs->apparmor_profile != next->apparmor_profile +#endif /* HAVE_APPARMOR */ || cs->runchroot != next->runchroot || cs->runcwd != next->runcwd; if (!quoted && !last_one) { diff --git a/plugins/sudoers/cvtsudoers_json.c b/plugins/sudoers/cvtsudoers_json.c index 10386617e..a89ef82b3 100644 --- a/plugins/sudoers/cvtsudoers_json.c +++ b/plugins/sudoers/cvtsudoers_json.c @@ -581,6 +581,9 @@ cmndspec_continues(struct cmndspec *cs, struct cmndspec *next) #ifdef HAVE_SELINUX && cs->role == next->role && cs->type == next->type #endif /* HAVE_SELINUX */ +#ifdef HAVE_APPARMOR + && cs->apparmor_profile == next->apparmor_profile +#endif /* HAVE_APPARMOR */ && cs->runchroot == next->runchroot && cs->runcwd == next->runcwd; return ret; } @@ -755,6 +758,16 @@ print_cmndspec_json(struct json_container *jsonc, } #endif /* HAVE_SELINUX */ +#ifdef HAVE_APPARMOR + if (cs->apparmor_profile != NULL) { + sudo_json_open_array(jsonc, "AppArmor_Spec"); + value.type = JSON_STRING; + value.u.string = cs->apparmor_profile; + sudo_json_add_value(jsonc, "apparmor_profile", &value); + sudo_json_close_array(jsonc); + } +#endif /* HAVE_APPARMOR */ + #ifdef HAVE_PRIV_SET /* Print Solaris privs/limitprivs */ if (cs->privs != NULL || cs->limitprivs != NULL) { diff --git a/plugins/sudoers/cvtsudoers_ldif.c b/plugins/sudoers/cvtsudoers_ldif.c index a71890b95..91acfcd86 100644 --- a/plugins/sudoers/cvtsudoers_ldif.c +++ b/plugins/sudoers/cvtsudoers_ldif.c @@ -460,6 +460,18 @@ print_cmndspec_ldif(FILE *fp, struct sudoers_parse_tree *parse_tree, } #endif /* HAVE_SELINUX */ +#ifdef HAVE_APPARMOR + /* Print AppArmor profile */ + if (cs->apparmor_profile != NULL) { + if (asprintf(&attr_val, "apparmor_profile=%s", cs->apparmor_profile) == -1) { + sudo_fatalx(U_("%s: %s"), __func__, + U_("unable to allocate memory")); + } + print_attribute_ldif(fp, "sudoOption", attr_val); + free(attr_val); + } +#endif /* HAVE_APPARMOR */ + #ifdef HAVE_PRIV_SET /* Print Solaris privs/limitprivs */ if (cs->privs != NULL || cs->limitprivs != NULL) { diff --git a/plugins/sudoers/cvtsudoers_merge.c b/plugins/sudoers/cvtsudoers_merge.c index 5093a0fa5..dde5b9b37 100644 --- a/plugins/sudoers/cvtsudoers_merge.c +++ b/plugins/sudoers/cvtsudoers_merge.c @@ -976,6 +976,14 @@ cmndspec_equivalent(struct cmndspec *cs1, struct cmndspec *cs2, bool check_negat debug_return_bool(false); } #endif +#ifdef HAVE_APPARMOR + if (cs1->apparmor_profile != NULL && cs2->apparmor_profile != NULL) { + if (strcmp(cs1->apparmor_profile, cs2->apparmor_profile) != 0) + debug_return_bool(false); + } else if (cs1->apparmor_profile != cs2->apparmor_profile) { + debug_return_bool(false); + } +#endif #ifdef HAVE_PRIV_SET if (cs1->privs != NULL && cs2->privs != NULL) { if (strcmp(cs1->privs, cs2->privs) != 0) diff --git a/plugins/sudoers/def_data.c b/plugins/sudoers/def_data.c index 3925d8c93..41de8fc0b 100644 --- a/plugins/sudoers/def_data.c +++ b/plugins/sudoers/def_data.c @@ -667,6 +667,10 @@ struct sudo_defs_types sudo_defs_table[] = { "intercept_type", T_TUPLE, N_("The mechanism used by the intercept and log_subcmds options: %s"), def_data_intercept_type, + }, { + "apparmor_profile", T_STR, + N_("AppArmor profile to use in the new security context: %s"), + NULL, }, { NULL, 0, NULL } diff --git a/plugins/sudoers/def_data.h b/plugins/sudoers/def_data.h index 7903e2418..a8deba41b 100644 --- a/plugins/sudoers/def_data.h +++ b/plugins/sudoers/def_data.h @@ -306,8 +306,10 @@ #define def_log_passwords (sudo_defs_table[I_LOG_PASSWORDS].sd_un.flag) #define I_PASSPROMPT_REGEX 152 #define def_passprompt_regex (sudo_defs_table[I_PASSPROMPT_REGEX].sd_un.list) -#define I_INTERCEPT_TYPE 153 +#define I_INTERCEPT_TYPE 154 #define def_intercept_type (sudo_defs_table[I_INTERCEPT_TYPE].sd_un.tuple) +#define I_APPARMOR_PROFILE 153 +#define def_apparmor_profile (sudo_defs_table[I_APPARMOR_PROFILE].sd_un.str) enum def_tuple { never, diff --git a/plugins/sudoers/def_data.in b/plugins/sudoers/def_data.in index f15b71544..6372048c9 100644 --- a/plugins/sudoers/def_data.in +++ b/plugins/sudoers/def_data.in @@ -479,3 +479,6 @@ intercept_type T_TUPLE "The mechanism used by the intercept and log_subcmds options: %s" dso trace +apparmor_profile + T_STR + "AppArmor profile to use in the new security context: %s" \ No newline at end of file diff --git a/plugins/sudoers/fmtsudoers.c b/plugins/sudoers/fmtsudoers.c index e786f6636..c4152efbf 100644 --- a/plugins/sudoers/fmtsudoers.c +++ b/plugins/sudoers/fmtsudoers.c @@ -241,6 +241,10 @@ sudoers_format_cmndspec(struct sudo_lbuf *lbuf, if (cs->type != NULL && FIELD_CHANGED(prev_cs, cs, type)) sudo_lbuf_append(lbuf, "TYPE=%s ", cs->type); #endif /* HAVE_SELINUX */ +#ifdef HAVE_APPARMOR + if (cs->apparmor_profile != NULL && FIELD_CHANGED(prev_cs, cs, apparmor_profile)) + sudo_lbuf_append(lbuf, "APPARMOR_PROFILE=%s ", cs->apparmor_profile); +#endif /* HAVE_APPARMOR */ if (cs->runchroot != NULL && FIELD_CHANGED(prev_cs, cs, runchroot)) sudo_lbuf_append(lbuf, "CHROOT=%s ", cs->runchroot); if (cs->runcwd != NULL && FIELD_CHANGED(prev_cs, cs, runcwd)) diff --git a/plugins/sudoers/gram.y b/plugins/sudoers/gram.y index aafe43afc..72b06efa2 100644 --- a/plugins/sudoers/gram.y +++ b/plugins/sudoers/gram.y @@ -144,6 +144,7 @@ static void alias_error(const char *name, int errnum); %token CWD /* working directory for command */ %token TYPE /* SELinux type */ %token ROLE /* SELinux role */ +%token APPARMOR_PROFILE /* AppArmor profile */ %token PRIVS /* Solaris privileges */ %token LIMITPRIVS /* Solaris limit privileges */ %token CMND_TIMEOUT /* command timeout */ @@ -182,6 +183,7 @@ static void alias_error(const char *name, int errnum); %type chrootspec %type rolespec %type typespec +%type apparmor_profilespec %type privsspec %type limitprivsspec %type timeoutspec @@ -534,6 +536,10 @@ cmndspec : runasspec options cmndtag digcmnd { cs->type = $2.type; parser_leak_remove(LEAK_PTR, $2.type); #endif +#ifdef HAVE_APPARMOR + cs->apparmor_profile = $2.apparmor_profile; + parser_leak_remove(LEAK_PTR, $2.apparmor_profile); +#endif #ifdef HAVE_PRIV_SET cs->privs = $2.privs; parser_leak_remove(LEAK_PTR, $2.privs); @@ -688,6 +694,11 @@ typespec : TYPE '=' WORD { } ; +apparmor_profilespec : APPARMOR_PROFILE '=' WORD { + $$ = $3; + } + ; + privsspec : PRIVS '=' WORD { $$ = $3; } @@ -783,6 +794,7 @@ reserved_word : ALL { $$ = "ALL"; } | TYPE { $$ = "TYPE"; } | PRIVS { $$ = "PRIVS"; } | LIMITPRIVS { $$ = "LIMITPRIVS"; } + | APPARMOR_PROFILE { $$ = "APPARMOR_PROFILE"; } ; reserved_alias : reserved_word { @@ -846,6 +858,13 @@ options : /* empty */ { parser_leak_remove(LEAK_PTR, $$.type); free($$.type); $$.type = $2; +#endif + } + | options apparmor_profilespec { +#ifdef HAVE_APPARMOR + parser_leak_remove(LEAK_PTR, $$.apparmor_profile); + free($$.apparmor_profile); + $$.apparmor_profile = $2; #endif } | options privsspec { diff --git a/plugins/sudoers/parse.c b/plugins/sudoers/parse.c index a69b67e52..a56f86c81 100644 --- a/plugins/sudoers/parse.c +++ b/plugins/sudoers/parse.c @@ -254,6 +254,24 @@ apply_cmndspec(struct cmndspec *cs) } } #endif /* HAVE_SELINUX */ +#ifdef HAVE_APPARMOR + /* Set AppArmor profile, if specified */ + if (cs->apparmor_profile != NULL) { + user_apparmor_profile = strdup(cs->apparmor_profile); + if (user_apparmor_profile == NULL) { + sudo_warnx(U_("%s: %s"), __func__, + U_("unable to allocate memory")); + debug_return_bool(false); + } + } else { + user_apparmor_profile = def_apparmor_profile; + def_apparmor_profile = NULL; + } + if (user_apparmor_profile != NULL) { + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "user_apparmor_profile -> %s", user_apparmor_profile); + } +#endif #ifdef HAVE_PRIV_SET /* Set Solaris privilege sets */ if (runas_privs == NULL) { @@ -525,6 +543,10 @@ new_long_entry(struct cmndspec *cs, struct cmndspec *prev_cs) if (cs->type && (!prev_cs->type || strcmp(cs->type, prev_cs->type) != 0)) debug_return_bool(true); #endif /* HAVE_SELINUX */ +#ifdef HAVE_APPARMOR + if (cs->apparmor_profile && (!prev_cs->apparmor_profile || strcmp(cs->apparmor_profile, prev_cs->apparmor_profile) != 0)) + debug_return_bool(true); +#endif /* HAVE_APPARMOR */ if (cs->runchroot && (!prev_cs->runchroot || strcmp(cs->runchroot, prev_cs->runchroot) != 0)) debug_return_bool(true); if (cs->runcwd && (!prev_cs->runcwd || strcmp(cs->runcwd, prev_cs->runcwd) != 0)) diff --git a/plugins/sudoers/parse.h b/plugins/sudoers/parse.h index ed15a3440..e276faad0 100644 --- a/plugins/sudoers/parse.h +++ b/plugins/sudoers/parse.h @@ -149,6 +149,9 @@ struct command_options { #ifdef HAVE_SELINUX char *role, *type; /* SELinux role and type */ #endif +#ifdef HAVE_APPARMOR + char *apparmor_profile; /* AppArmor profile */ +#endif #ifdef HAVE_PRIV_SET char *privs, *limitprivs; /* Solaris privilege sets */ #endif @@ -233,6 +236,9 @@ struct cmndspec { #ifdef HAVE_SELINUX char *role, *type; /* SELinux role and type */ #endif +#ifdef HAVE_APPARMOR + char *apparmor_profile; /* AppArmor profile */ +#endif #ifdef HAVE_PRIV_SET char *privs, *limitprivs; /* Solaris privilege sets */ #endif diff --git a/plugins/sudoers/policy.c b/plugins/sudoers/policy.c index 5308cefa9..080889eb6 100644 --- a/plugins/sudoers/policy.c +++ b/plugins/sudoers/policy.c @@ -328,6 +328,16 @@ sudoers_policy_deserialize_info(void *v, struct defaults_list *defaults) continue; } #endif /* HAVE_SELINUX */ +#ifdef HAVE_APPARMOR + if (MATCHES(*cur, "apparmor_profile=")) { + CHECK(*cur, "apparmor_profile="); + free(user_apparmor_profile); + user_apparmor_profile = strdup(*cur + sizeof("apparmor_profile=") - 1); + if (user_apparmor_profile == NULL) + goto oom; + continue; + } +#endif /* HAVE_APPARMOR */ #ifdef HAVE_BSD_AUTH_H if (MATCHES(*cur, "bsdauth_type=")) { CHECK(*cur, "bsdauth_type="); @@ -957,6 +967,12 @@ sudoers_policy_store_result(bool accepted, char *argv[], char *envp[], goto oom; } #endif /* HAVE_SELINUX */ +#ifdef HAVE_APPARMOR + if (user_apparmor_profile != NULL) { + if ((command_info[info_len++] = sudo_new_key_val("apparmor_profile", user_apparmor_profile)) == NULL) + goto oom; + } +#endif /* HAVE_APPARMOR */ #ifdef HAVE_PRIV_SET if (runas_privs != NULL) { if ((command_info[info_len++] = sudo_new_key_val("runas_privs", runas_privs)) == NULL) diff --git a/plugins/sudoers/regress/fuzz/fuzz_policy.dict b/plugins/sudoers/regress/fuzz/fuzz_policy.dict index 48678c853..c031eb9ac 100644 --- a/plugins/sudoers/regress/fuzz/fuzz_policy.dict +++ b/plugins/sudoers/regress/fuzz/fuzz_policy.dict @@ -30,6 +30,7 @@ "runas_limitprivs" "selinux_role" "selinux_type" +"apparmor_profile" "bsdauth_type" "network_addrs" "max_groups" diff --git a/plugins/sudoers/regress/fuzz/fuzz_sudoers.dict b/plugins/sudoers/regress/fuzz/fuzz_sudoers.dict index ea90c49ee..93ef8e7a5 100644 --- a/plugins/sudoers/regress/fuzz/fuzz_sudoers.dict +++ b/plugins/sudoers/regress/fuzz/fuzz_sudoers.dict @@ -55,6 +55,7 @@ "NOTAFTER" "ROLE" "TYPE" +"APPARMOR_PROFILE" "PRIVS" "LIMITPRIVS" @@ -127,6 +128,7 @@ "env_keep" "role" "type" +"apparmor_profile" "env_file" "restricted_env_file" "sudoers_locale" diff --git a/plugins/sudoers/sudoers.c b/plugins/sudoers/sudoers.c index 1f1e22389..71e8ba256 100644 --- a/plugins/sudoers/sudoers.c +++ b/plugins/sudoers/sudoers.c @@ -1825,6 +1825,9 @@ sudo_user_free(void) free(user_role); free(user_type); #endif +#ifdef HAVE_APPARMOR + free(user_apparmor_profile); +#endif #ifdef HAVE_PRIV_SET free(runas_privs); free(runas_limitprivs); diff --git a/plugins/sudoers/sudoers.h b/plugins/sudoers/sudoers.h index 4bf922ac4..c506c57c4 100644 --- a/plugins/sudoers/sudoers.h +++ b/plugins/sudoers/sudoers.h @@ -111,6 +111,9 @@ struct sudo_user { char *role; char *type; #endif +#ifdef HAVE_APPARMOR + char *apparmor_profile; +#endif #ifdef HAVE_PRIV_SET char *privs; char *limitprivs; @@ -248,6 +251,7 @@ struct sudo_user { #define runas_gr (sudo_user._runas_gr) #define user_role (sudo_user.role) #define user_type (sudo_user.type) +#define user_apparmor_profile (sudo_user.apparmor_profile) #define user_closefrom (sudo_user.closefrom) #define runas_privs (sudo_user.privs) #define runas_limitprivs (sudo_user.limitprivs) diff --git a/plugins/sudoers/toke.l b/plugins/sudoers/toke.l index 91280739b..3cd4eecb8 100644 --- a/plugins/sudoers/toke.l +++ b/plugins/sudoers/toke.l @@ -679,6 +679,14 @@ ALL { goto got_alias; #endif } +APPARMOR_PROFILE { +#ifdef HAVE_APPARMOR + LEXTRACE("APPARMOR_PROFILE "); + return APPARMOR_PROFILE; +#else + goto got_alias; +#endif + } PRIVS { #ifdef HAVE_PRIV_SET LEXTRACE("PRIVS "); diff --git a/scripts/mkdep.pl b/scripts/mkdep.pl index 3d6e6b65e..380439384 100755 --- a/scripts/mkdep.pl +++ b/scripts/mkdep.pl @@ -115,7 +115,7 @@ sub mkdep { # Expand some configure bits $makefile =~ s:\@DEV\@::g; $makefile =~ s:\@COMMON_OBJS\@:aix.lo event_poll.lo event_select.lo:; - $makefile =~ s:\@SUDO_OBJS\@:intercept.pb-c.o openbsd.o preload.o selinux.o sesh.o solaris.o:; + $makefile =~ s:\@SUDO_OBJS\@:intercept.pb-c.o openbsd.o preload.o apparmor.o selinux.o sesh.o solaris.o:; $makefile =~ s:\@SUDOERS_OBJS\@:bsm_audit.lo linux_audit.lo ldap.lo ldap_util.lo ldap_conf.lo solaris_audit.lo sssd.lo:; # XXX - fill in AUTH_OBJS from contents of the auth dir instead $makefile =~ s:\@AUTH_OBJS\@:afs.lo aix_auth.lo bsdauth.lo dce.lo fwtk.lo getspwuid.lo kerb5.lo pam.lo passwd.lo rfc1938.lo secureware.lo securid5.lo sia.lo:; diff --git a/scripts/mkpkg b/scripts/mkpkg index 294a77723..b477c8053 100755 --- a/scripts/mkpkg +++ b/scripts/mkpkg @@ -269,6 +269,23 @@ case "$osversion" in make_opts="${make_opts}${make_opts+ }"'docdir=$(datarootdir)/doc/packages/$(PACKAGE_TARNAME)' ;; deb*|ubu*) + # AppArmor is enabled by default starting in + # Debian: Debian 10 (Buster) + # Ubuntu: Ubuntu 12.04 (Precise Pangolin) + osmajor=`sed -n -e 's/^VERSION_ID=\"\([0-9]*\).*$/\1/p' /etc/os-release` + case "$osversion" in + deb*) + if [ -z $osmajor ] || [ $osmajor -ge 10 ]; then + with_apparmor=true + fi + ;; + ubu*) + if [ -z $osmajor ] || [ $osmajor -ge 14 ]; then + with_apparmor=true + fi + ;; + esac + # Encrypted remote I/O log support. with_openssl=true # Python plugins @@ -295,6 +312,9 @@ case "$osversion" in configure_opts="${configure_opts}${configure_opts+$tab}--with-sssd-lib=/usr/lib/$MULTIARCH" fi fi + if [ X"$with_apparmor" = X"true" ]; then + configure_opts="${configure_opts}${configure_opts+$tab}--with-apparmor" + fi configure_opts="--prefix=/usr --with-all-insults --with-pam @@ -311,7 +331,6 @@ case "$osversion" in --with-sendmail=/usr/sbin/sendmail --mandir=/usr/share/man --libexecdir=/usr/lib - --with-selinux --with-linux-audit $configure_opts" # Use correct libaudit dependency diff --git a/src/Makefile.in b/src/Makefile.in index f6ff40532..9882923ba 100644 --- a/src/Makefile.in +++ b/src/Makefile.in @@ -378,6 +378,24 @@ sudo_noexec.lo: $(srcdir)/sudo_noexec.c $(incdir)/sudo_compat.h \ $(LIBTOOL) $(LTFLAGS) --mode=compile $(CC) -c $(CPPFLAGS) $(CFLAGS) $(PIE_CFLAGS) $(HARDENING_CFLAGS) $(srcdir)/sudo_noexec.c # Autogenerated dependencies, do not modify +apparmor.o: $(srcdir)/apparmor.c $(incdir)/compat/stdbool.h \ + $(incdir)/sudo_compat.h $(incdir)/sudo_conf.h \ + $(incdir)/sudo_debug.h $(incdir)/sudo_event.h \ + $(incdir)/sudo_fatal.h $(incdir)/sudo_gettext.h \ + $(incdir)/sudo_plugin.h $(incdir)/sudo_queue.h \ + $(incdir)/sudo_util.h $(srcdir)/sudo.h $(top_builddir)/config.h \ + $(top_builddir)/pathnames.h + $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/apparmor.c +apparmor.i: $(srcdir)/apparmor.c $(incdir)/compat/stdbool.h \ + $(incdir)/sudo_compat.h $(incdir)/sudo_conf.h \ + $(incdir)/sudo_debug.h $(incdir)/sudo_event.h \ + $(incdir)/sudo_fatal.h $(incdir)/sudo_gettext.h \ + $(incdir)/sudo_plugin.h $(incdir)/sudo_queue.h \ + $(incdir)/sudo_util.h $(srcdir)/sudo.h $(top_builddir)/config.h \ + $(top_builddir)/pathnames.h + $(CC) -E -o $@ $(CPPFLAGS) $< +apparmor.plog: apparmor.i + rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/apparmor.c --i-file $< --output-file $@ check_net_ifs.o: $(srcdir)/regress/net_ifs/check_net_ifs.c \ $(incdir)/compat/stdbool.h $(incdir)/sudo_compat.h \ $(incdir)/sudo_util.h $(top_builddir)/config.h diff --git a/src/apparmor.c b/src/apparmor.c new file mode 100644 index 000000000..5f36f3064 --- /dev/null +++ b/src/apparmor.c @@ -0,0 +1,111 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2022 Will Shand + * + * 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. + */ + +#include + +#ifdef HAVE_APPARMOR + +# include +# include +# include + +# include "sudo.h" +# include "sudo_debug.h" + +/** + * @brief Check whether AppArmor is enabled. + * + * @return 1 if AppArmor is enabled, 0 otherwise. + */ +int +apparmor_is_enabled(void) +{ + int ret; + FILE *fd; + debug_decl(apparmor_is_enabled, SUDO_DEBUG_APPARMOR); + + /* Check whether AppArmor is enabled by reading + * /sys/module/apparmor/parameters/enabled + * + * When this file exists and its contents are equal to "Y", AppArmor + * is enabled. This is a little more reliable than using + * aa_is_enabled(2), which performs an additional check on securityfs + * that will fail in settings where securityfs isn't available + * (e.g. inside a container). + */ + + fd = fopen("/sys/module/apparmor/parameters/enabled", "r"); + if (fd == NULL) + debug_return_int(0); + + ret = (fgetc(fd) == 'Y'); + + fclose(fd); + debug_return_int(ret); +} + +/** + * @brief Prepare to transition into a new AppArmor profile. + * + * @param new_profile The AppArmor profile to transition into on the + * next exec. + * + * @return 0 on success, and a nonzero value on failure. + */ +int +apparmor_prepare(const char *new_profile) +{ + int ret; + char *mode, *old_profile; + debug_decl(apparmor_prepare, SUDO_DEBUG_APPARMOR); + + /* Determine the current AppArmor confinement status */ + if ((ret = aa_getcon(&old_profile, &mode)) == -1) { + sudo_warn("%s", U_("failed to determine AppArmor confinement")); + old_profile = NULL; + goto done; + } + + /* Tell AppArmor to transition into the new profile on the + * next exec */ + if ((ret = aa_change_onexec(new_profile)) != 0) { + sudo_warn(U_("unable to change AppArmor profile to %s"), new_profile); + goto done; + } + + if (mode == NULL) + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: changing AppArmor profile: %s -> %s", __func__, + old_profile, new_profile ? new_profile : "?" + ); + else + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: changing AppArmor profile: %s (%s) -> %s", __func__, + old_profile, mode, new_profile ? new_profile : "?" + ); + +done: + /* The profile string returned by aa_getcon must be free'd, while the + * mode string must _not_ be free'd */ + if (old_profile != NULL) + free(old_profile); + + debug_return_int(ret); +} + +#endif /* HAVE_APPARMOR */ diff --git a/src/parse_args.c b/src/parse_args.c index b1dfba3b2..7a8da9209 100644 --- a/src/parse_args.c +++ b/src/parse_args.c @@ -83,6 +83,7 @@ static struct sudo_settings sudo_settings[] = { { "askpass" }, { "intercept_setid" }, { "intercept_ptrace" }, + { "apparmor_profile" }, { NULL } }; diff --git a/src/sudo.c b/src/sudo.c index 96de4db64..4f0f8fa35 100644 --- a/src/sudo.c +++ b/src/sudo.c @@ -689,6 +689,9 @@ command_info_to_details(char * const info[], struct command_details *details) for (i = 0; info[i] != NULL; i++) { sudo_debug_printf(SUDO_DEBUG_INFO, " %d: %s", i, info[i]); switch (info[i][0]) { + case 'a': + SET_STRING("apparmor_profile=", apparmor_profile); + break; case 'c': SET_STRING("chroot=", chroot) SET_STRING("command=", command) @@ -895,6 +898,15 @@ command_info_to_details(char * const info[], struct command_details *details) exit(EXIT_FAILURE); } #endif + +#ifdef HAVE_APPARMOR + if (details->apparmor_profile != NULL && apparmor_is_enabled()) { + i = apparmor_prepare(details->apparmor_profile); + if (i != 0) + exit(EXIT_FAILURE); + } +#endif + debug_return; } diff --git a/src/sudo.h b/src/sudo.h index 2cfce0aa4..aa018ccd6 100644 --- a/src/sudo.h +++ b/src/sudo.h @@ -104,6 +104,7 @@ #define ARG_ASKPASS 25 #define ARG_INTERCEPT_SETID 26 #define ARG_INTERCEPT_PTRACE 27 +#define ARG_APPARMOR_PROFILE 28 /* * Flags for tgetpass() @@ -199,6 +200,7 @@ struct command_details { const char *chroot; const char *selinux_role; const char *selinux_type; + const char *apparmor_profile; const char *utmp_user; const char *tty; char **argv; @@ -286,6 +288,10 @@ int selinux_setexeccon(void); void selinux_execve(int fd, const char *path, char *const argv[], char *envp[], int flags); +/* apparmor.c */ +int apparmor_is_enabled(void); +int apparmor_prepare(const char* new_profile); + /* solaris.c */ void set_project(struct passwd *); int os_init_solaris(int argc, char *argv[], char *envp[]);