On 2021/06/14 19:40, Martijn van Duren wrote:
> On Mon, 2021-06-14 at 12:55 +0100, Stuart Henderson wrote:
> > By default, snmpd responds to the frequently abused community strings
> > "public" and "private".
> >
> > To prevent this, at present you must either use "seclevel auth" or
> > "seclevel enc" (if you would like to only use SNMPv3), set an explicit
> > string for the read-only community, or set either an explicit string
> > or "disabled" for the read-write community.
> >
> > I would like to remove the defaults. If somebody really wants to use
> > the strings "public" or "private" they can set them themselves. The
> > internet doesn't need any more unintentional UDP amplifiers than
> > necessary.
>
> I fully agree.
> >
> > Additionally if somebody goes to the trouble of configuring SNMPv3,
> > the common use case is to want authentication+encryption; anything
> > wider than that, it's reasonable to expect the user to make it
> > explicit, so I've changed it to "seclevel enc" by default iff an
> > SNMPv3 user is created.
>
> > This works as expected in the use-cases I've tried. Any concerns/
> > comments/OKs? (I'll write an faq/current.html entry if it goes in).
> >
> > Note, it's not possible to require auth+enc for SNMPv3 requests while
> > also allowing SNMPv1/2; that isn't new and I haven't changed it.
> >
> I think that your approach is a nice first stab, but is incomplete.
thanks for looking.
> - if the concern is amplification attacks then setting the minlevel to
> authpriv is too high, since you'll silently break logins for users
> that miss the enckey parameter.
> I changed this to always default to seclevel auth.
I do still think enc is the safer default (i.e. "the user has to do
something to weaken things") though I don't strongly object to auth
instead of enc.
> - Make sure that no users can be created with lower privs then seclevel,
> so that people don't have to start debugging this issue when already
> running in production with loglevels that are always too low.
nice.
> - If we disable SNMPv{1,2c} by default I reckon it's dumb to overload
> seclevel with said protocols. I decoupled them (this addresses your
> last statement)
> - Add a disallow for empty community strings. This effectively
> disables SNMPv{1,2c} in all cases if we set an empty string.
agreed.
> - Log the disabling of SNMPv{1,2c} properly and increment
> snmp_inbadversions.
this feels like it's overloading snmp_inbadversions a bit,
previously this refers to things which are actually invalid
and since ro and rw can be disabled independently it's hard to
give an accurate log message.
> - Setting an empty string is equal to disallow, so make
> "read-write community" equal to an empty string and remove the now
> useless sc_readonly.
> - When sc_trcommunity is disabled we don't have the backup for
> "trap receiver". Check for this explicitly during startup.
makes sense.
> At some point I want to rework the MPS subsystem (which snmpe.c
> basically mostly is). But I think this diff is a nice stopgap until
> I get around to it.
>
> Only lightly tested through regress and written quickly in between
> jobs, so OKs welcome, but some modifications before commit can't
> be ruled out. :-)
>
> martijn@
from a first read through:
> Index: parse.y
> ===================================================================
> RCS file: /cvs/src/usr.sbin/snmpd/parse.y,v
> retrieving revision 1.63
> diff -u -p -r1.63 parse.y
> --- parse.y 22 Jan 2021 06:33:26 -0000 1.63
> +++ parse.y 14 Jun 2021 17:39:47 -0000
> @@ -217,7 +217,7 @@ main : LISTEN ON listenproto
> free($3);
> }
> | READWRITE DISABLED {
> - conf->sc_readonly = 1;
> + conf->sc_rwcommunity[0] = '\0';
> }
> | TRAP COMMUNITY STRING {
> if (strlcpy(conf->sc_trcommunity, $3,
> @@ -1102,7 +1102,10 @@ parse_config(const char *filename, u_int
> struct sockaddr_storage ss;
> struct sym *sym, *next;
> struct address *h;
> - int found;
> + struct trap_address *tr;
> + const struct usmuser *up;
> + const char *errstr;
> + int found;
>
> if ((conf = calloc(1, sizeof(*conf))) == NULL) {
> log_warn("%s", __func__);
> @@ -1112,10 +1115,8 @@ parse_config(const char *filename, u_int
> conf->sc_flags = flags;
> conf->sc_confpath = filename;
> TAILQ_INIT(&conf->sc_addresses);
> - strlcpy(conf->sc_rdcommunity, "public", SNMPD_MAXCOMMUNITYLEN);
> - strlcpy(conf->sc_rwcommunity, "private", SNMPD_MAXCOMMUNITYLEN);
> - strlcpy(conf->sc_trcommunity, "public", SNMPD_MAXCOMMUNITYLEN);
> TAILQ_INIT(&conf->sc_trapreceivers);
> + conf->sc_min_seclevel = SNMP_MSGFLAG_AUTH;
>
> if ((file = pushfile(filename, 0)) == NULL) {
> free(conf);
> @@ -1130,6 +1131,8 @@ parse_config(const char *filename, u_int
>
> endservent();
>
> + if ((up = usm_check_mincred(conf->sc_min_seclevel, &errstr)) != NULL)
> + fatalx("user '%s': %s", up->uu_name, errstr);
> /* Setup default listen addresses */
> if (TAILQ_EMPTY(&conf->sc_addresses)) {
> if (host("0.0.0.0", SNMP_PORT, SOCK_DGRAM, &ss, 1) != 1)
> @@ -1155,6 +1158,17 @@ parse_config(const char *filename, u_int
> log_warnx("notify listener needs at least one trap handler");
> free(conf);
> return (NULL);
> + }
> +
> + if (conf->sc_trcommunity[0] == '\0') {
> + found = 0;
> + TAILQ_FOREACH(tr, &conf->sc_trapreceivers, entry) {
> + if (tr->sa_community == NULL) {
> + log_warnx("trap receiver: missing community");
> + free(conf);
> + return (NULL);
> + }
> + }
> }
>
> /* Free macros and check which have not been used. */
> Index: snmpd.conf.5
> ===================================================================
> RCS file: /cvs/src/usr.sbin/snmpd/snmpd.conf.5,v
> retrieving revision 1.48
> diff -u -p -r1.48 snmpd.conf.5
> --- snmpd.conf.5 14 Jun 2021 12:28:58 -0000 1.48
> +++ snmpd.conf.5 14 Jun 2021 17:39:47 -0000
> @@ -136,35 +136,27 @@ set requires at least one
> statement.
> .It Ic read-only community Ar string
> Specify the name of the read-only community.
> -The default value is
> -.Ar public .
> -.It Ic read-write Pq Ic community Ar string Ic | disabled
> +There is no default value.
> +.It Ic read-write Ic community Ar string
> Specify the name of the read-write community, or disallow writes completely.
> -The default value is
> -.Ar private .
> +There is no default value.
> .It Ic seclevel Pq Ic none | auth | enc
> Specify the lowest security level that
> .Xr snmpd 8
> -accepts:
> +accepts on SNMPv3:
> .Bl -tag -width "auth" -offset ident
> .It Ic none
> Both authentication and encryption of messages is optional.
> -This is the default value.
> .It Ic auth
> Authentication of messages is mandatory.
> .Xr snmpd 8
> will discard any messages that don't have a valid digest.
> Encryption of messages is optional.
> +This is the default value.
> .It Ic enc
> Messages must be encrypted and must have a valid digest for authentication.
> Otherwise they will be discarded.
> .El
> -.Pp
> -If the chosen value is different from
> -.Ic none
> -.Xr snmpd 8
> -will accept only SNMPv3 requests since older versions neither support
> -authentication nor encryption.
> .It Ic system contact Ar string
> Specify the name or description of the system contact, typically a
> name or an email address.
> @@ -206,8 +198,7 @@ description in the SNMP MIB for details.
> .\"XXX describe the complicated services alg here
> .It Ic trap community Ar string
> Specify the name of the trap community.
> -The default value is
> -.Ar public .
> +There is no default value.
> .It Ic trap handle Ar oid Qq Ar command
> Execute
> .Ic command
> @@ -330,6 +321,7 @@ to listen on localhost, override the def
> magic services value and provides some custom OID values:
> .Bd -literal -offset indent
> listen on 127.0.0.1
> +read community public
mistake carried over from my diff; should be "read-only community"
>
> system oid 1.3.6.1.4.1.30155.23.2
> system services 74
> Index: snmpd.h
> ===================================================================
> RCS file: /cvs/src/usr.sbin/snmpd/snmpd.h,v
> retrieving revision 1.95
> diff -u -p -r1.95 snmpd.h
> --- snmpd.h 20 May 2021 08:53:12 -0000 1.95
> +++ snmpd.h 14 Jun 2021 17:39:47 -0000
> @@ -576,7 +576,6 @@ struct snmpd {
> int sc_pfaddrfilter;
>
> int sc_min_seclevel;
> - int sc_readonly;
> int sc_traphandler;
>
> struct privsep sc_ps;
> @@ -740,6 +739,7 @@ struct ber_element *usm_encode(struct sn
> struct ber_element *usm_encrypt(struct snmp_message *, struct ber_element *);
> void usm_finalize_digest(struct snmp_message *, char *, ssize_t);
> void usm_make_report(struct snmp_message *);
> +const struct usmuser *usm_check_mincred(int, const char **);
>
> /* proc.c */
> enum privsep_procid
> Index: snmpe.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/snmpd/snmpe.c,v
> retrieving revision 1.71
> diff -u -p -r1.71 snmpe.c
> --- snmpe.c 20 May 2021 08:53:12 -0000 1.71
> +++ snmpe.c 14 Jun 2021 17:39:47 -0000
> @@ -255,14 +255,13 @@ snmpe_parse(struct snmp_message *msg)
> switch (msg->sm_version) {
> case SNMP_V1:
> case SNMP_V2:
> - if (env->sc_min_seclevel != 0)
> - goto badversion;
> if (ober_scanf_elements(a, "seS$", &comn, &msg->sm_pdu) != 0)
> goto parsefail;
> if (strlcpy(msg->sm_community, comn,
> - sizeof(msg->sm_community)) >= sizeof(msg->sm_community)) {
> + sizeof(msg->sm_community)) >= sizeof(msg->sm_community) ||
> + msg->sm_community[0] == '\0') {
> stats->snmp_inbadcommunitynames++;
> - msg->sm_errstr = "community name too long";
> + msg->sm_errstr = "invalid community name";
> goto fail;
> }
> break;
> @@ -295,9 +294,9 @@ snmpe_parse(struct snmp_message *msg)
> msg->sm_ctxname[len] = '\0';
> break;
> default:
> - badversion:
> - stats->snmp_inbadversions++;
> msg->sm_errstr = "bad snmp version";
> +badversion:
> + stats->snmp_inbadversions++;
> goto fail;
> }
>
> @@ -330,10 +329,17 @@ snmpe_parse(struct snmp_message *msg)
> msg->sm_errstr = "read requests disabled";
> goto fail;
> }
> + if (env->sc_rdcommunity[0] == '\0' &&
> + env->sc_rwcommunity[0] == '\0') {
> + if (msg->sm_version == SNMP_V1 ||
> + msg->sm_version == SNMP_V2) {
> + msg->sm_errstr = "community read disabled";
> + goto badversion;
> + }
> + }
english word order: "read community disabled". i'm not quite sure about
using this errstr/counter though. RO and RW can be disabled independently
and it seems wrong to print "read disabled" only if _both_ RO+RW are
disabled otherwise "wrong read community" if read is disabled and
write is allowed.
on the other hand, this (unset sc_rdcommunity and sc_rwcommunity) is
exactly the case when someone upgrades without allowing for the change,
and they're the main people who will see such a message.
stylistic nit, i'm not a fan of mixing "if (msg->sm_version == SNMP_V1 ||
msg->sm_version == SNMP_V2)" with the existing "if (msg->sm_version !=
SNMP_V3)", they both mean the same at this point (as there's a switch
above) but it seems like unneeded extra cognitive load for the reader
to use 2 different conditionals meaning the same thing. something
like this feels a better fit with the surrounding code to me,
if (msg->sm_version != SNMP_V3 &&
env->sc_rdcommunity[0] == '\0' &&
env->sc_rwcommunity[0] == '\0') {
msg->sm_errstr = "read community disabled";
> if (msg->sm_version != SNMP_V3 &&
> strcmp(env->sc_rdcommunity, msg->sm_community) != 0 &&
> - (env->sc_readonly ||
> - strcmp(env->sc_rwcommunity, msg->sm_community) != 0)) {
> + strcmp(env->sc_rwcommunity, msg->sm_community) != 0) {
> stats->snmp_inbadcommunitynames++;
> msg->sm_errstr = "wrong read community";
> goto fail;
> @@ -346,9 +352,15 @@ snmpe_parse(struct snmp_message *msg)
> msg->sm_errstr = "write requests disabled";
now that there are more things which can be disabled (community as well
as address), i think it would make sense to add "for address" for this
(and the similar errstr for read/notify)
> goto fail;
> }
> + if (env->sc_rwcommunity[0] == '\0') {
> + if (msg->sm_version == SNMP_V1 ||
> + msg->sm_version == SNMP_V2) {
> + msg->sm_errstr = "community write disabled";
> + goto badversion;
> + }
> + }
we're only dealing with the rw community here so my wondering above
about errstr (which i'm undecided about) doesn't apply. my == / != nit
and word ordering "write community disabled" does though.
> if (msg->sm_version != SNMP_V3 &&
> - (env->sc_readonly ||
> - strcmp(env->sc_rwcommunity, msg->sm_community) != 0)) {
> + strcmp(env->sc_rwcommunity, msg->sm_community) != 0) {
> if (strcmp(env->sc_rdcommunity, msg->sm_community) != 0)
> stats->snmp_inbadcommunitynames++;
> else
> @@ -384,6 +396,13 @@ snmpe_parse(struct snmp_message *msg)
> msg->sm_errstr = "SNMPv3 doesn't support traps yet";
> goto fail;
> }
> + if (env->sc_trcommunity[0] == '\0') {
> + if (msg->sm_version == SNMP_V1 ||
> + msg->sm_version == SNMP_V2) {
we are !SNMP_V3 only here because of the above "SNMPv3 doesn't support
traps yet" goto fail, and the block below is also one that would need
!= SNMP_V3 if the above "goto fail" was changed, so i think it would
make sense to either drop the version check, or add a check to the
block below.
> + msg->sm_errstr = "community notify disabled";
this community is defined with config command "trap community" so
probably better use that instead of notify, "trap community disabled"
> + goto badversion;
> + }
> + }
> if (strcmp(env->sc_trcommunity, msg->sm_community) != 0) {
> stats->snmp_inbadcommunitynames++;
> msg->sm_errstr = "wrong trap community";
> @@ -498,19 +517,16 @@ snmpe_parsevarbinds(struct snmp_message
> stats->snmp_intotalreqvars++;
> break;
> case SNMP_C_SETREQ:
> - if (snmpd_env->sc_readonly == 0) {
> - /*
> - * XXX A set varbind should only be committed if
> - * all variables are staged
> - */
> - if (mps_setreq(msg, value, &o) == 0) {
> - /* XXX Adjust after fixing staging */
> - stats->snmp_intotalsetvars++;
> - break;
> - }
> + /*
> + * XXX A set varbind should only be committed if
> + * all variables are staged
> + */
> + if (mps_setreq(msg, value, &o) != 0) {
> + msg->sm_error = SNMP_ERROR_READONLY;
> + goto varfail;
> }
> - msg->sm_error = SNMP_ERROR_READONLY;
> - goto varfail;
> + stats->snmp_intotalsetvars++;
> + break;
> case SNMP_C_GETBULKREQ:
> rvarbind = NULL;
> if (mps_getbulkreq(msg, &rvarbind, &end, &o,
> Index: usm.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/snmpd/usm.c,v
> retrieving revision 1.19
> diff -u -p -r1.19 usm.c
> --- usm.c 20 May 2021 08:53:12 -0000 1.19
> +++ usm.c 14 Jun 2021 17:39:47 -0000
> @@ -177,6 +177,27 @@ usm_newuser(char *name, const char **err
> return up;
> }
>
> +const struct usmuser *
> +usm_check_mincred(int minlevel, const char **errstr)
> +{
> + struct usmuser *up;
> +
> + if (minlevel == 0)
> + return NULL;
> +
> + SLIST_FOREACH(up, &usmuserlist, uu_next) {
> + if (minlevel & SNMP_MSGFLAG_PRIV && up->uu_privkey == NULL) {
> + *errstr = "missing enckey";
> + return up;
> + }
> + if (minlevel & SNMP_MSGFLAG_AUTH && up->uu_authkey == NULL) {
> + *errstr = "missing authkey";
> + return up;
> + }
> + }
> + return NULL;
> +}
> +
> struct usmuser *
> usm_finduser(char *name)
> {
>
>
and i'll try to have another read through and actually test it
in the morning :)