Index | Thread | Search

From:
Rafael Sadowski <rafael@sizeofvoid.org>
Subject:
httpd: add header block/drop rules for request filtering
To:
tech@openbsd.org
Date:
Mon, 21 Sep 2026 16:02:08 +0200

Download raw body.

Thread
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) {