Index | Thread | Search

From:
Kirill A. Korinsky <kirill@korins.ky>
Subject:
Re: httpd: add header block/drop rules for request filtering
To:
Rafael Sadowski <rafael@sizeofvoid.org>
Cc:
tech@openbsd.org
Date:
Mon, 21 Sep 2026 22:18:22 +0200

Download raw body.

Thread
On Mon, 21 Sep 2026 16:02:08 +0200,
Rafael Sadowski <rafael@sizeofvoid.org> wrote:
> 
> Hi tech@
> 
> during EuroBSDCon26, Purple Rain (secbsd.com) came up to me and showed
> me his httpd diff. The idea was to block Ai scrapers by the user-agents
> header because his site had crashed under the load.
> 
> My answer was that you can also do this with relayd(8) in front of
> http(8). However, I understand that not everyone wants to run relayd(8)
> for a single httpd.
> 
> His idea was to do this for user agents. I incorporated the whole thing
> into our "header" syntax and made it generic. Now you can block (with an
> HTTP status code) or drop anything request you want based on key/value
> header pairs.
> 
> Here are a few use cases:
> 
> 1.) Drop all the Ai scrapers
> 
> $ cat ai_scrapers.conf
> # BLOCK AI CRAWLERS AND TRAINING
> 
> header drop "user-agent" "*bot*"
> header block "user-agent" "addsearchbot*" 403
> header block "user-agent" "agenttimes*" 403
> header block "user-agent" "ai2bot*" 403
> header block "user-agent" "aihitbot*" 403
> header block "user-agent" "aiwebindex*" 403
> header block "user-agent" "amazon*" 403
> header block "user-agent" "amzn*" 403
> header block "user-agent" "andibot*" 403
> header block "user-agent" "anomura*" 403
> header drop "user-agent" "anthropic*"
> header block "user-agent" "apify*" 403
> header block "user-agent" "applebot*" 403
> header block "user-agent" "aranet*" 403
> header block "user-agent" "atlassian-bot*" 403
> header block "user-agent" "awario*" 403
> header block "user-agent" "azureai*" 403
> ...
> 
> server "default" {
> 	listen on * port 80
> 	# block Ai and crawlers
> 	include "/etc/ai_scrapers.conf"
>  	location "/*" {
> 		root "/htdocs/localhost"
> 	}
> }
> 
> 2.) block with redirect
> 
> header block "user-agent" "amazon*" 301 "https://amazon.com"
> 
> 2.) block with message
> 
> header block "user-agent" "amazon*" 404 "bye bye my love"
> 
> I'm not sure if we want this in 8.0 or if we should wait until after the
> release. Of course, it would be useful to have it in the release.
> 
> Purple Rain tested this diff in production. (Thanks)
> 
> Feedback welcome.
>

It reads interesting not sure how it can be used against AI crawlers,
because shity one never respects User Agent, and sane one moves to proove
who they are by using Web Bot Auth.

Anyway, I don't object from this feature, and actually it can be useful for someone.


> Rafael
> 
> commit 1cb5c4a126af863bbb62b5594a1cf2253167cde6
> Author: Rafael Sadowski <rafael@sizeofvoid.org>
> Date:   Tue Sep 15 18:43:00 2026 +0200
> 
>     httpd: add header block/drop rules for request filtering
>     
>     With this incoming requests can also be rejected based on the value of a
>     request header. Valid options are:
>     
>     block name value code [arg]
>             Close the connection with an error response when a
>             request header matches.  Both name and value are shell-
>             style patterns and are matched case-insensitively against
>             the header name and value.  code must be a valid HTTP
>             status code.  For codes in the 3xx range, arg is required
>             and sent as the "Location" header.  It must start with
>             "http://" or "https://".  For all other codes, arg is
>             optional and used as the log message identifying the
>             rule.
>     
>     drop name value
>             Silently close the connection without sending a response
>             when a request header matches, using the same pattern
>             rules as block.
>     
>     Based on a diff from Purple Rain from SecBSD, who wrote a initial
>     version to block Ai- and other Scraper. Also requested and tested
>     by Mischa.
> 
> diff --git a/config.c b/config.c
> index ecaa59a..c7b933f 100644
> --- a/config.c
> +++ b/config.c
> @@ -198,6 +198,7 @@ clear_config_server_ptrs(struct server_config *cfg)
>  	/* clear TAILQ_HEAD */
>  	memset(&cfg->fcgiparams, 0, sizeof(cfg->fcgiparams));
>  	memset(&cfg->headers, 0, sizeof(cfg->headers));
> +	memset(&cfg->header_rules, 0, sizeof(cfg->header_rules));
>  
>  	/* clear TAILQ_ENTRY */
>  	memset(&cfg->entry, 0, sizeof(cfg->entry));
> @@ -295,6 +296,11 @@ config_setserver(struct httpd *env, struct server *srv)
>  	if (config_setserver_headers(env, srv) == -1)
>  		return (-1);
>  
> +	/* Configure headers rules if necessary. */
> +	config_inherit_header_rules(env, srv);
> +	if (config_setserver_header_rules(env, srv) == -1)
> +		return (-1);
> +
>  	/* Close server socket early to prevent fd exhaustion in the parent. */
>  	if (srv->srv_s != -1) {
>  		close(srv->srv_s);
> @@ -521,6 +527,124 @@ config_inherit_headers(struct httpd *env, struct server *srv)
>  	TAILQ_CONCAT(&srv_conf->headers, &inherited, entry);
>  }
>  
> +int
> +config_getserver_header_rules(struct httpd *env, struct imsg *imsg)
> +{
> +	struct server_config	*srv_conf;
> +	struct header_rule	*rule;
> +	struct header_rule_imsg	 hmsg;
> +	struct ibuf		 ibuf;
> +
> +	if (imsg_get_ibuf(imsg, &ibuf) == -1 ||
> +	    ibuf_get(&ibuf, &hmsg, sizeof(hmsg)) == -1) {
> +		log_debug("%s: invalid message", __func__);
> +		return (-1);
> +	}
> +
> +	if ((srv_conf = serverconfig_byid(hmsg.id)) == NULL) {
> +		log_debug("%s: invalid config id", __func__);
> +		return (-1);
> +	}
> +
> +	if ((rule = calloc(1, sizeof(*rule))) == NULL)
> +		fatal("header rule out of memory");
> +
> +	rule->name = ibuf_get_string(&ibuf, hmsg.namelen);
> +	rule->value = ibuf_get_string(&ibuf, hmsg.vallen);
> +	rule->return_uri = ibuf_get_string(&ibuf, hmsg.urilen);
> +
> +	if (rule->name == NULL || rule->value == NULL ||
> +	    rule->return_uri == NULL) {
> +		free(rule->name);
> +		free(rule->value);
> +		free(rule->return_uri);
> +		free(rule);
> +		return (-1);
> +	}
> +	rule->action = hmsg.action;
> +	rule->return_code = hmsg.return_code;
> +
> +	TAILQ_INSERT_TAIL(&srv_conf->header_rules, rule, entry);
> +	return (0);
> +}
> +
> +/*
> + * Inherit header rules from parent server
> + */
> +void
> +config_inherit_header_rules(struct httpd *env, struct server *srv)
> +{
> +	struct server			*parent_srv;
> +	struct server_config		*srv_conf = &srv->srv_conf;
> +	struct header_rule		*rule, *nrule;
> +	struct server_header_rules	 inherited;
> +
> +	if (!(srv_conf->flags & SRVFLAG_LOCATION))
> +		return;
> +
> +	/* Find parent server by parent_id */
> +	TAILQ_FOREACH(parent_srv, env->sc_servers, srv_entry) {
> +		if (parent_srv->srv_conf.id == srv_conf->parent_id)
> +			break;
> +	}
> +
> +	if (parent_srv == NULL)
> +		return;
> +
> +	TAILQ_INIT(&inherited);
> +
> +	TAILQ_FOREACH(rule, &parent_srv->srv_conf.header_rules, entry) {
> +		nrule = header_rule_dup(rule);
> +		TAILQ_INSERT_TAIL(&inherited, nrule, entry);
> +		DPRINTF("%s: inheriting header rule \"%s\" from parent \"%s\" "
> +		    "to location \"%s\"", __func__, rule->name,
> +		    parent_srv->srv_conf.name, srv_conf->location);
> +	}
> +
> +	TAILQ_CONCAT(&srv_conf->header_rules, &inherited, entry);
> +}
> +
> +int
> +config_setserver_header_rules(struct httpd *env, struct server *srv)
> +{
> +	struct privsep		*ps = env->sc_ps;
> +	struct server_config	*srv_conf = &srv->srv_conf;
> +	struct header_rule	*rule;
> +	struct header_rule_imsg	 hmsg;
> +	struct iovec		 iov[4];
> +
> +	DPRINTF("%s: sending header rules for \"%s[%u]\" to %s fd %d",
> +	    __func__, srv_conf->name, srv_conf->id, ps->ps_title[PROC_SERVER],
> +	    srv->srv_s);
> +
> +	TAILQ_FOREACH(rule, &srv_conf->header_rules, entry) {
> +		hmsg.id = srv_conf->id;
> +
> +		hmsg.namelen = strlen(rule->name);
> +		hmsg.vallen = strlen(rule->value);
> +		hmsg.return_code = rule->return_code;
> +		hmsg.action = rule->action;
> +		hmsg.urilen = strlen(rule->return_uri);
> +
> +		iov[0].iov_base = &hmsg;
> +		iov[0].iov_len = sizeof(hmsg);
> +		iov[1].iov_base = rule->name;
> +		iov[1].iov_len = hmsg.namelen;
> +		iov[2].iov_base = rule->value;
> +		iov[2].iov_len = hmsg.vallen;
> +		iov[3].iov_base = rule->return_uri;
> +		iov[3].iov_len = hmsg.urilen;
> +
> +		if (proc_composev(ps, PROC_SERVER, IMSG_CFG_HEADER_RULES,
> +		    iov, 4) != 0) {
> +			log_warn("%s: failed to compose IMSG_CFG_HEADER_RULES "
> +			    "for `%s'", __func__, srv_conf->name);
> +			return (-1);
> +		}
> +	}
> +	return (0);
> +}
> +
>  int
>  config_setserver_headers(struct httpd *env, struct server *srv)
>  {
> @@ -842,6 +966,7 @@ config_getserver(struct httpd *env, struct imsg *imsg)
>  	srv->srv_s = fd;
>  
>  	TAILQ_INIT(&srv->srv_conf.headers);
> +	TAILQ_INIT(&srv->srv_conf.header_rules);
>  	TAILQ_INIT(&srv->srv_conf.fcgiparams);
>  
>  	if (config_getserver_auth(env, &srv->srv_conf) != 0)
> diff --git a/httpd.c b/httpd.c
> index 738fca2..20c2da8 100644
> --- a/httpd.c
> +++ b/httpd.c
> @@ -1284,3 +1284,22 @@ header_dup(const struct custom_header *src)
>  	h->flags = src->flags;
>  	return (h);
>  }
> +
> +struct header_rule *
> +header_rule_dup(const struct header_rule *src)
> +{
> +	struct header_rule *r;
> +
> +	if ((r = calloc(1, sizeof(*r))) == NULL)
> +		fatal("out of memory");
> +	if ((r->name = strdup(src->name)) == NULL ||
> +	    (r->value = strdup(src->value)) == NULL)
> +		fatal("out of memory");
> +
> +	r->action = src->action;
> +	r->return_code = src->return_code;
> +	if ((r->return_uri = strdup(src->return_uri)) == NULL)
> +		fatal("out of memory");
> +
> +	return (r);
> +}
> diff --git a/httpd.conf.5 b/httpd.conf.5
> index c288692..b6339f0 100644
> --- a/httpd.conf.5
> +++ b/httpd.conf.5
> @@ -535,6 +535,39 @@ block are inherited from the
>  context and override defined headers with the same name.
>  If you do not wish to inherit these, you can remove them again with
>  .Ic remove .
> +.Pp
> +Incoming requests can also be rejected based on the value of a request
> +header.
> +Valid options are:
> +.Bl -tag -width Ds
> +.It Ic block Ar name Ar value Ar code Op Ar arg
> +Close the connection with an error response when a request header
> +matches.
> +Both
> +.Ar name
> +and
> +.Ar value
> +are shell-style patterns and are matched case-insensitively against
> +the header name and value.
> +.Ar code
> +must be a valid HTTP status code.
> +For codes in the 3xx range,
> +.Ar arg
> +is required and sent as the
> +.Qq Location
> +header.
> +It must start with
> +.Qq http://
> +or
> +.Qq https:// .
> +For all other codes,
> +.Ar arg
> +is optional and used as the log message identifying the rule.
> +.It Ic drop Ar name Ar value
> +Silently close the connection without sending a response when a request
> +header matches, using the same pattern rules as
> +.Ic block .
> +.El
>  .It Ic hsts Oo Ar option Oc
>  Enable HTTP Strict Transport Security.
>  Valid options are:
> diff --git a/httpd.h b/httpd.h
> index 71377cf..821eca2 100644
> --- a/httpd.h
> +++ b/httpd.h
> @@ -190,6 +190,7 @@ enum imsg_type {
>  	IMSG_CFG_AUTH,
>  	IMSG_CFG_FCGI,
>  	IMSG_CFG_HEADERS,
> +	IMSG_CFG_HEADER_RULES,
>  	IMSG_CFG_DONE,
>  	IMSG_LOG_ACCESS,
>  	IMSG_LOG_ERROR,
> @@ -413,6 +414,12 @@ enum log_format {
>  	LOG_FORMAT_FORWARDED
>  };
>  
> +enum header_action {
> +	HEADER_ACTION_DROP,
> +	HEADER_ACTION_RETURN,
> +	HEADER_ACTION_RDR
> +};
> +
>  #define HEADER_REMOVE		0x01
>  #define HEADER_ADD		0x02
>  #define HEADER_SET		0x04
> @@ -425,6 +432,15 @@ struct header_imsg {
>  	uint16_t	vallen;
>  };
>  
> +struct header_rule_imsg {
> +	uint32_t		id;		/* server conf id */
> +	uint32_t		namelen;
> +	uint32_t		vallen;
> +	uint32_t		action;
> +	uint32_t		return_code;
> +	uint32_t		urilen;
> +};
> +
>  struct log_file {
>  	char			log_name[PATH_MAX];
>  	int			log_fd;
> @@ -478,6 +494,17 @@ struct custom_header {
>  };
>  TAILQ_HEAD(server_headers, custom_header);
>  
> +struct header_rule {
> +	char				*name;
> +	char				*value;
> +	enum header_action		 action;
> +	u_int32_t			 return_code;
> +	char				*return_uri;
> +
> +	TAILQ_ENTRY(header_rule)	 entry;
> +};
> +TAILQ_HEAD(server_header_rules, header_rule);
> +
>  struct server_config {
>  	uint32_t			 id;
>  	uint32_t			 parent_id;
> @@ -549,6 +576,7 @@ struct server_config {
>  	struct server_fcgiparams	 fcgiparams;
>  	int				 fcgistrip;
>  	int				 fcgiallowchunked;
> +	struct server_header_rules	 header_rules;
>  	struct server_headers		 headers;
>  	char				 errdocroot[HTTPD_ERRDOCROOT_MAX];
>  
> @@ -634,6 +662,7 @@ int			 server_privinit(struct server *);
>  void			 server_purge(struct server *);
>  void			 serverconfig_free(struct server_config *);
>  void			 server_headers_free(struct server_headers *);
> +void			 server_header_rules_free(struct server_header_rules *);
>  void			 serverconfig_reset(struct server_config *);
>  int			 server_socket_af(struct sockaddr_storage *, in_port_t);
>  in_port_t		 server_socket_getport(struct sockaddr_storage *);
> @@ -678,6 +707,8 @@ void			 server_abort_http(struct client *, unsigned int,
>      const char *);
>  int			 server_custom_headers(struct server_config *,
>      struct kvtree *, unsigned int);
> +struct header_rule	*server_match_header_rule(struct server_config *,
> +    struct http_descriptor *);
>  enum httpmethod		 server_httpmethod_byname(const char *);
>  const char		*server_httpmethod_byid(unsigned int);
>  const char		*server_httperror_byid(unsigned int);
> @@ -762,6 +793,7 @@ void			 print_custom_header(const char *,
>      const struct custom_header *);
>  int			 header_exists(struct server_config *, const char *);
>  struct custom_header	*header_dup(const struct custom_header *);
> +struct header_rule	*header_rule_dup(const struct header_rule *);
>  
>  extern struct httpd *httpd_env;
>  
> @@ -810,10 +842,13 @@ int	 config_setserver(struct httpd *, struct server *);
>  int	 config_setserver_tls(struct httpd *, struct server *);
>  int	 config_setserver_fcgiparams(struct httpd *, struct server *);
>  int	 config_setserver_headers(struct httpd *, struct server *);
> +int	 config_setserver_header_rules(struct httpd *, struct server *);
>  void	 config_inherit_headers(struct httpd *, struct server *);
> +void	 config_inherit_header_rules(struct httpd *, struct server *);
>  int	 config_getserver(struct httpd *, struct imsg *);
>  int	 config_getserver_tls(struct httpd *, struct imsg *);
>  int	 config_getserver_fcgiparams(struct httpd *, struct imsg *);
> +int	 config_getserver_header_rules(struct httpd *, struct imsg *);
>  int	 config_getserver_headers(struct httpd *, struct imsg *);
>  int	 config_setmedia(struct httpd *, struct media_type *);
>  int	 config_getmedia(struct httpd *, struct imsg *);
> diff --git a/parse.y b/parse.y
> index 204a2dc..e1c1e9c 100644
> --- a/parse.y
> +++ b/parse.y
> @@ -341,6 +341,7 @@ server		: SERVER optmatch STRING	{
>  			TAILQ_INIT(&srv->srv_hosts);
>  			TAILQ_INIT(&srv_conf->fcgiparams);
>  			TAILQ_INIT(&srv_conf->headers);
> +			TAILQ_INIT(&srv_conf->header_rules);
>  
>  			TAILQ_INSERT_TAIL(&srv->srv_hosts, srv_conf, entry);
>  		} '{' optnl serveropts_l '}'	{
> @@ -664,6 +665,7 @@ serveroptsl	: LISTEN ON STRING opttls port	{
>  			srv_conf = &srv->srv_conf;
>  			SPLAY_INIT(&srv->srv_clients);
>  			TAILQ_INIT(&srv_conf->headers);
> +			TAILQ_INIT(&srv_conf->header_rules);
>  			TAILQ_INIT(&srv_conf->fcgiparams);
>  		} '{' optnl serveropts_l '}'	{
>  			struct server	*s = NULL;
> @@ -832,6 +834,91 @@ header		: HEADER REMOVE STRING optalways	{
>  				hdr->flags |= HEADER_ALWAYS;
>  			TAILQ_INSERT_TAIL(&srv->srv_conf.headers, hdr, entry);
>  		}
> +		| HEADER BLOCK STRING STRING NUMBER optstring {
> +			struct header_rule	*hrule;
> +
> +			if ((hrule= calloc(1, sizeof(*hrule))) == NULL)
> +				fatal("out of memory");
> +
> +			hrule->action = HEADER_ACTION_RETURN;
> +
> +			hrule->name = $3;
> +			hrule->value = $4;
> +
> +			if ($5 < 100 || $5 >= 600) {
> +				log_warn("header rule return code number is"
> +				"outside of a valid range");
> +			}
> +
> +			hrule->return_code = $5;
> +
> +			if (hrule->return_code >= 300 &&
> +			    hrule->return_code <= 399) {
> +				hrule->action = HEADER_ACTION_RDR;
> +			}
> +
> +			switch (hrule->action) {
> +			case HEADER_ACTION_DROP:
> +				/* Handeled in header drop sysntax */
> +				break;
> +			case HEADER_ACTION_RDR:
> +				if ($6 == NULL) {
> +					yyerror("missing return URI");
> +					free($6);
> +					free(hrule->name);
> +					free(hrule->value);
> +					free(hrule);
> +					YYERROR;
> +				}
> +				hrule->return_uri = $6;
> +				break;
> +			case HEADER_ACTION_RETURN:
> +				hrule->return_uri = ($6 != NULL) ? $6 :
> +				strdup("blocked");
> +				if (hrule->return_uri == NULL) {
> +					yyerror("out of memory");
> +					free(hrule->name);
> +					free(hrule->value);
> +					free(hrule);
> +					YYERROR;
> +				}
> +				break;
> +			default:
> +				break;
> +			}
> +
> +			if (hrule->action == HEADER_ACTION_RDR &&
> +			    (strncmp(hrule->return_uri, "http://", 7) != 0 &&
> +			     strncmp(hrule->return_uri, "https://", 8) != 0)) {
> +				yyerror("Redirect URI not starts with "
> +				"http:// or https://");
> +				free(hrule->name);
> +				free(hrule->value);
> +				free(hrule->return_uri);
> +				free(hrule);
> +				YYERROR;
> +			}
> +
> +			TAILQ_INSERT_TAIL(&srv->srv_conf.header_rules, hrule, entry);
> +		}
> +		| HEADER DROP STRING STRING {
> +			struct header_rule	*hrule;
> +
> +			if ((hrule= calloc(1, sizeof(*hrule))) == NULL)
> +				fatal("out of memory");
> +
> +			hrule->action = HEADER_ACTION_DROP;
> +			if ((hrule->return_uri = strdup("dropped")) == NULL) {
> +				yyerror("out of memory");
> +				free(hrule);
> +				YYERROR;
> +			}
> +
> +			hrule->name = $3;
> +			hrule->value = $4;
> +
> +			TAILQ_INSERT_TAIL(&srv->srv_conf.header_rules, hrule, entry);
> +		}
>  		;
>  
>  optfound	: /* empty */	{ $$ = 0; }
> @@ -2480,6 +2567,7 @@ server_inherit(struct server *src, struct server_config *alias,
>  {
>  	struct server	*dst, *s, *dstl;
>  	struct custom_header	*hdr, *nhdr;
> +	struct header_rule *rule, *nrule;
>  
>  	if ((dst = calloc(1, sizeof(*dst))) == NULL)
>  		fatal("out of memory");
> @@ -2493,6 +2581,12 @@ server_inherit(struct server *src, struct server_config *alias,
>  		TAILQ_INSERT_TAIL(&dst->srv_conf.headers, nhdr, entry);
>  	}
>  
> +	TAILQ_INIT(&dst->srv_conf.header_rules);
> +	TAILQ_FOREACH(rule, &src->srv_conf.header_rules, entry) {
> +		nrule = header_rule_dup(rule);
> +		TAILQ_INSERT_TAIL(&dst->srv_conf.header_rules, nrule, entry);
> +	}
> +
>  	if ((dst->srv_conf.tls_cert_file =
>  	    strdup(src->srv_conf.tls_cert_file)) == NULL)
>  		fatal("out of memory");
> @@ -2590,6 +2684,13 @@ server_inherit(struct server *src, struct server_config *alias,
>  			TAILQ_INSERT_TAIL(&dstl->srv_conf.headers, nhdr, entry);
>  		}
>  
> +		/* Copy header rules from source location */
> +		TAILQ_INIT(&dstl->srv_conf.header_rules);
> +		TAILQ_FOREACH(rule, &s->srv_conf.header_rules, entry) {
> +			nrule = header_rule_dup(rule);
> +			TAILQ_INSERT_TAIL(&dstl->srv_conf.header_rules, nrule, entry);
> +		}
> +
>  		strlcpy(dstl->srv_conf.name, alias->name,
>  		    sizeof(dstl->srv_conf.name));
>  
> diff --git a/server.c b/server.c
> index bc96722..9aab25c 100644
> --- a/server.c
> +++ b/server.c
> @@ -472,6 +472,7 @@ server_purge(struct server *srv)
>  	}
>  
>  	server_headers_free(&srv->srv_conf.headers);
> +	server_header_rules_free(&srv->srv_conf.header_rules);
>  	tls_config_free(srv->srv_tls_config);
>  	tls_free(srv->srv_tls_ctx);
>  
> @@ -490,6 +491,19 @@ server_headers_free(struct server_headers *headers)
>  	}
>  }
>  
> +void
> +server_header_rules_free(struct server_header_rules *rules)
> +{
> +	struct header_rule *rule, *trule;
> +
> +	TAILQ_FOREACH_SAFE(rule, rules, entry, trule) {
> +		free(rule->name);
> +		free(rule->value);
> +		free(rule->return_uri);
> +		free(rule);
> +	}
> +}
> +
>  void
>  serverconfig_free(struct server_config *srv_conf)
>  {
> @@ -513,6 +527,7 @@ serverconfig_free(struct server_config *srv_conf)
>  		free(param);
>  	}
>  	server_headers_free(&srv_conf->headers);
> +	server_header_rules_free(&srv_conf->header_rules);
>  }
>  
>  void
> @@ -532,6 +547,7 @@ serverconfig_reset(struct server_config *srv_conf)
>  	srv_conf->tls_ocsp_staple_file = NULL;
>  	TAILQ_INIT(&srv_conf->fcgiparams);
>  	TAILQ_INIT(&srv_conf->headers);
> +	TAILQ_INIT(&srv_conf->header_rules);
>  }
>  
>  struct server *
> @@ -1395,6 +1411,10 @@ server_dispatch_parent(int fd, struct privsep_proc *p, struct imsg *imsg)
>  		if (config_getserver_headers(httpd_env, imsg) != 0)
>  			return (-1);
>  		break;
> +	case IMSG_CFG_HEADER_RULES:
> +		if (config_getserver_header_rules(httpd_env, imsg) != 0)
> +			return (-1);
> +		break;
>  	case IMSG_CFG_DONE:
>  		if (config_getcfg(httpd_env, imsg) != 0)
>  			return (-1);
> diff --git a/server_http.c b/server_http.c
> index 52cbc5d..8e47be2 100644
> --- a/server_http.c
> +++ b/server_http.c
> @@ -52,6 +52,8 @@ int		 server_http_authenticate(struct server_config *,
>      struct client *);
>  static int	 http_version_num(char *);
>  static int	 http_is_success(unsigned int code);
> +static int	 match_header_rule(struct header_rule *, const char *,
> +    const char *);
>  char		*server_expand_http(struct client *, const char *,
>      char *, size_t);
>  char		*replace_var(char *, const char *, const char *);
> @@ -229,6 +231,37 @@ http_is_success(unsigned int code)
>  	return (code >= 200 && code < 400);
>  }
>  
> +static int
> +match_header_rule(struct header_rule *rule, const char *key, const char *value)
> +{
> +	return (fnmatch(rule->name, key, FNM_CASEFOLD) == 0 &&
> +	    fnmatch(rule->value, value, FNM_CASEFOLD) == 0);
> +}
> +
> +struct header_rule *
> +server_match_header_rule(struct server_config *srv_conf,
> +    struct http_descriptor *desc)
> +{
> +	struct header_rule	*rule;
> +	struct kv		*hdr = NULL;
> +	struct kv		*kv = NULL;
> +
> +	RB_FOREACH(hdr, kvtree, &desc->http_headers) {
> +		TAILQ_FOREACH(rule, &srv_conf->header_rules, entry) {
> +			if (match_header_rule(rule, hdr->kv_key, hdr->kv_value))
> +				return (rule);
> +		}
> +		TAILQ_FOREACH(kv, &hdr->kv_children, kv_entry) {
> +			TAILQ_FOREACH(rule, &srv_conf->header_rules, entry) {
> +				if (match_header_rule(rule, kv->kv_key,
> +				    kv->kv_value))
> +					return (rule);
> +			}
> +		}
> +	}
> +	return (NULL);
> +}
> +
>  void
>  server_read_http(struct bufferevent *bev, void *arg)
>  {
> @@ -1354,6 +1387,7 @@ server_response(struct httpd *httpd, struct client *clt)
>  	int			 portval = -1, ret;
>  	char			*hostval, *query;
>  	const char		*errstr = NULL;
> +	struct header_rule	*mrule = NULL;
>  
>  	/* Preserve original path */
>  	if (desc->http_path == NULL ||
> @@ -1474,6 +1508,23 @@ server_response(struct httpd *httpd, struct client *clt)
>  		server_abort_http(clt, 500, desc->http_path);
>  		return (-1);
>  	}
> +	if ((mrule = server_match_header_rule(srv_conf, desc)) != NULL) {
> +		switch (mrule->action) {
> +		case HEADER_ACTION_DROP:
> +			server_close(clt, mrule->return_uri);
> +			return (-1);
> +		case HEADER_ACTION_RDR:
> +			server_abort_http(clt, mrule->return_code,
> +			    mrule->return_uri);
> +			return (-1);
> +		case HEADER_ACTION_RETURN:
> +			server_abort_http(clt, mrule->return_code,
> +			    mrule->return_uri);
> +			return (-1);
> +		default:
> +			break;
> +		}
> +	}
>  
>  	/* Optional rewrite */
>  	if (srv_conf->flags & SRVFLAG_PATH_REWRITE) {
> 

-- 
wbr, Kirill