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