Index | Thread | Search

From:
Mischa <openbsd@mlst.nl>
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 18:49:56 +0200

Download raw body.

Thread
Hi Rafael,

Thank you for the reminder!
Running it as we speak, and already like it a lot!

Mischa

On 2026-09-21 16:02, Rafael Sadowski 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.
> 
> 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) {