Index | Thread | Search

From:
Rafael Sadowski <rafael@sizeofvoid.org>
Subject:
Re: httpd: add custom HTTP header support #2
To:
tech@openbsd.org
Cc:
"Anthony J. Bentley" <bentley@openbsd.org>, "Kirill A. Korinsky" <kirill@korins.ky>, Stuart Henderson <stu@spacehopper.org>
Date:
Wed, 18 Mar 2026 17:48:54 +0100

Download raw body.

Thread
On Wed Feb 11, 2026 at 12:59:27AM +0100, Rafael Sadowski wrote:
> This is the second attempt to add custom HTTP header support to httpd.
> 
> After receiving initial feedback and advice from Stuart, I changed and
> improved the whole idea (This made it very powerful but also somewhat more
> complex to test) and the syntax:
> 
> Allow httpd.conf to set/add/remove custom HTTP response headers or
> suppress existing ones. This enables httpd to add security headers,
> custom metadata, or remove unwanted headers without modifying
> app/fastcgi code.
> 
> Three new directives are added:
> 
> header set name value [always]
>         Set a custom HTTP response header with the specified name
>         and value.  If a header with the same name is already
>         present in the response, its value will be replaced.  The
>         header is added to successful responses (2xx and 3xx
>         status codes) by default.
> 
> header add name value [always]
>         Add a custom HTTP response header with the specified name
>         and value.  Unlike set, this option appends the header
>         even if one with the same name already exists, allowing
>         for multiple headers with the same name.  The header is
>         added to successful responses (2xx and 3xx status codes)
>         by default.
> 
> header remove name
>         Suppress the HTTP response header with the specified
>         name.  This can be used to remove headers added by
>         default, such as "Last-Modified", as well as headers
>         inherited from a parent server configuration.
> 
> If always is specified, the header will be included in all
> responses, including error pages (4xx and 5xx status codes).
> 
> Header names are limited to 127 characters and values to 511
> characters.  Headers defined in a location block are inherited
> from the server context and override defined headers with the
> same name.  If you do not wish to inherit these, you can remove
> them again with remove.
> 
> Example configuration:
> 
>     server "example.com" {
>         listen on * tls port 443
> 
>         header add "X-Frame-Options" "SAMEORIGIN" always
>         header add "X-Content-Type-Options" "nosniff" always
>         header set "X-Powered-By" "OpenBSD httpd"
> 
>         location "/api/*" {
> 			# This location defines its own headers
> 			header add "Access-Control-Allow-Origin" "*"
> 			# Override from server context
> 			header set "Set-Cookie" "id=5; HttpOnly; Secure"
> 			# Remove from server context
> 			header remove "X-Frame-Options"
>         }
>     }
> 
> The implementation follows the existing pattern form FastCGI parameters.
> So I didn't have to invent anything new. Much of the code is from
> existing patterns (so the potential bugs too). Just to give one example:
> The entire IMSG part.
> 
> The motivation was to have something similar to ngx_http_headers_module
> without relayd coming into play. For this reason I follow the nginx pattern:
> 
> "Adds the specified field to a response header provided that the
> response code equals 200, 201 (1.3.10), 204, 206, 301, 302, 303, 304,
> 307 (1.1.16, 1.0.13), or 308 (1.13.0). Parameter value can contain
> variables."
> -- https://nginx.org/en/docs/http/ngx_http_headers_module.html
> 
> I tried to test all combinations (and sure I missed some) but feel
> comfortable with them. My goal was not to modify current behavior.
> 
> I would like to ask for more and more tests (big wild setups) and maybe
> OKs.

Is anyone interested?

> 
> Rafael
> 
> diff --git a/usr.sbin/httpd/config.c b/usr.sbin/httpd/config.c
> index 300a5f2caca..5c10f4218f1 100644
> --- a/usr.sbin/httpd/config.c
> +++ b/usr.sbin/httpd/config.c
> @@ -196,6 +196,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));
>  
>  	/* clear TAILQ_ENTRY */
>  	memset(&cfg->entry, 0, sizeof(cfg->entry));
> @@ -270,7 +271,11 @@ config_setserver(struct httpd *env, struct server *srv)
>  
>  			/* Configure TLS if necessary. */
>  			config_setserver_tls(env, srv);
> -		} else {
> +			/* Configure custom headers if necessary. */
> +			config_setserver_headers(env, srv);
> +
> +		} else if (id == PROC_SERVER &&
> +			   (srv->srv_conf.flags & SRVFLAG_LOCATION)) {
>  			if (proc_composev(ps, id, IMSG_CFG_SERVER,
>  			    iov, c) != 0) {
>  				log_warn("%s: failed to compose "
> @@ -278,7 +283,20 @@ config_setserver(struct httpd *env, struct server *srv)
>  				    __func__, srv->srv_conf.name);
>  				return (-1);
>  			}
> +			/* Configure FCGI parameters if necessary. */
> +			config_setserver_fcgiparams(env, srv);
>  
> +			/* Configure custom headers if necessary. */
> +			config_inherit_headers(env, srv);
> +			config_setserver_headers(env, srv);
> +		} else {
> +			if (proc_composev(ps, id, IMSG_CFG_SERVER,
> +			    iov, c) != 0) {
> +				log_warn("%s: failed to compose "
> +				    "IMSG_CFG_SERVER imsg for `%s'",
> +				    __func__, srv->srv_conf.name);
> +				return (-1);
> +			}
>  			/* Configure FCGI parameters if necessary. */
>  			config_setserver_fcgiparams(env, srv);
>  		}
> @@ -442,6 +460,162 @@ config_setserver_fcgiparams(struct httpd *env, struct server *srv)
>  	return (0);
>  }
>  
> +int
> +config_getserver_headers(struct httpd *env, struct imsg *imsg)
> +{
> +	struct server		*srv;
> +	struct server_config	*srv_conf, *iconf;
> +	struct custom_header	*hdr;
> +	uint32_t		 id;
> +	size_t			 c, nc, len;
> +	uint8_t			*p = imsg->data;
> +
> +	len = sizeof(nc) + sizeof(id);
> +	if (IMSG_DATA_SIZE(imsg) < len) {
> +		log_debug("%s: invalid message length", __func__);
> +		return (-1);
> +	}
> +
> +	memcpy(&nc, p, sizeof(nc));	/* number of headers */
> +	p += sizeof(nc);
> +
> +	memcpy(&id, p, sizeof(id));	/* server conf id */
> +	srv_conf = serverconfig_byid(id);
> +	p += sizeof(id);
> +
> +	len += nc*sizeof(*hdr);
> +	if (IMSG_DATA_SIZE(imsg) < len) {
> +		log_debug("%s: invalid message length", __func__);
> +		return (-1);
> +	}
> +
> +	/* Find associated server config */
> +	TAILQ_FOREACH(srv, env->sc_servers, srv_entry) {
> +		if (srv->srv_conf.id == id) {
> +			srv_conf = &srv->srv_conf;
> +			break;
> +		}
> +		TAILQ_FOREACH(iconf, &srv->srv_hosts, entry) {
> +			if (iconf->id == id) {
> +				srv_conf = iconf;
> +				break;
> +			}
> +		}
> +	}
> +
> +	/* Fetch custom headers */
> +	for (c = 0; c < nc; c++) {
> +		if ((hdr = calloc(1, sizeof(*hdr))) == NULL)
> +			fatalx("headers out of memory");
> +		memcpy(hdr, p, sizeof(*hdr));
> +		TAILQ_INSERT_HEAD(&srv_conf->headers, hdr, entry);
> +
> +		p += sizeof(*hdr);
> +	}
> +
> +	return (0);
> +}
> +
> +static int
> +header_exists(struct server_config *srv_conf, const char *name)
> +{
> +	struct custom_header	*hdr;
> +
> +	TAILQ_FOREACH(hdr, &srv_conf->headers, entry) {
> +		if (strcasecmp(hdr->name, name) == 0)
> +			return (1);
> +	}
> +	return (0);
> +}
> +
> +/*
> + * Inherit headers from parent server, skipping those
> + * already defined in the location.
> + */
> +void
> +config_inherit_headers(struct httpd *env, struct server *srv)
> +{
> +	struct server		*parent_srv;
> +	struct server_config	*srv_conf = &srv->srv_conf;
> +	struct custom_header	*hdr, *hdr_copy;
> +
> +	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_FOREACH(hdr, &parent_srv->srv_conf.headers, entry) {
> +		if (header_exists(srv_conf, hdr->name)) {
> +			DPRINTF("%s: skipping header \"%s\" from parent "
> +			    "\"%s\", overridden in location \"%s\"",
> +			    __func__, hdr->name,
> +			    parent_srv->srv_conf.name, srv_conf->location);
> +			continue;
> +		}
> +
> +		if ((hdr_copy = calloc(1, sizeof(*hdr_copy))) == NULL)
> +			fatal("out of memory");
> +
> +		strlcpy(hdr_copy->name, hdr->name, sizeof(hdr_copy->name));
> +		strlcpy(hdr_copy->value, hdr->value, sizeof(hdr_copy->value));
> +		hdr_copy->flags = hdr->flags;
> +
> +		TAILQ_INSERT_TAIL(&srv_conf->headers, hdr_copy, entry);
> +		DPRINTF("%s: inheriting header \"%s\" from parent \"%s\" "
> +		    "to location \"%s\"", __func__, hdr->name,
> +		    parent_srv->srv_conf.name, srv_conf->location);
> +	}
> +}
> +
> +int
> +config_setserver_headers(struct httpd *env, struct server *srv)
> +{
> +	struct privsep		*ps = env->sc_ps;
> +	struct server_config	*srv_conf = &srv->srv_conf;
> +	struct custom_header	*hdr;
> +	struct iovec		*iov;
> +	size_t			 c = 0, nc = 0;
> +
> +	DPRINTF("%s: sending headers for \"%s[%u]\" to %s fd %d", __func__,
> +	    srv_conf->name, srv_conf->id, ps->ps_title[PROC_SERVER],
> +	    srv->srv_s);
> +
> +	if (TAILQ_EMPTY(&srv_conf->headers))	/* nothing to do */
> +		return (0);
> +
> +	TAILQ_FOREACH(hdr, &srv_conf->headers, entry) {
> +		nc++;
> +	}
> +	if ((iov = calloc(nc + 2, sizeof(*iov))) == NULL)
> +		return (-1);
> +
> +	iov[c].iov_base = &nc;			/* number of headers */
> +	iov[c++].iov_len = sizeof(nc);
> +	iov[c].iov_base = &srv_conf->id;	/* server config id */
> +	iov[c++].iov_len = sizeof(srv_conf->id);
> +
> +	TAILQ_FOREACH(hdr, &srv_conf->headers, entry) {	/* push headers */
> +		iov[c].iov_base = hdr;
> +		iov[c++].iov_len = sizeof(*hdr);
> +	}
> +	if (proc_composev(ps, PROC_SERVER, IMSG_CFG_HEADERS, iov, c) != 0) {
> +		log_warn("%s: failed to compose IMSG_CFG_HEADERS imsg for "
> +		    "`%s'", __func__, srv_conf->name);
> +		free(iov);
> +		return (-1);
> +	}
> +	free(iov);
> +
> +	return (0);
> +}
> +
>  int
>  config_setserver_tls(struct httpd *env, struct server *srv)
>  {
> diff --git a/usr.sbin/httpd/httpd.conf.5 b/usr.sbin/httpd/httpd.conf.5
> index 3053709a359..72d80628ac6 100644
> --- a/usr.sbin/httpd/httpd.conf.5
> +++ b/usr.sbin/httpd/httpd.conf.5
> @@ -464,6 +464,57 @@ Enable static gzip compression to save bandwidth.
>  If gzip encoding is accepted and if the requested file exists with
>  an additional .gz suffix, use the compressed file instead and deliver
>  it with content encoding gzip.
> +.It Ic header Ar option
> +Manipulate HTTP response headers.
> +Multiple
> +.Ic header
> +statements may be specified.
> +Valid options are:
> +.Bl -tag -width Ds
> +.It Ic set Ar name Ar value Op Ic always
> +Set a custom HTTP response header with the specified
> +.Ar name
> +and
> +.Ar value .
> +If a header with the same
> +.Ar name
> +is already present in the response, its value will be replaced.
> +The header is added to successful responses
> +(2xx and 3xx status codes) by default.
> +.It Ic add Ar name Ar value Op Ic always
> +Add a custom HTTP response header with the specified
> +.Ar name
> +and
> +.Ar value .
> +Unlike
> +.Ic set ,
> +this option appends the header even if one with the same
> +.Ar name
> +already exists, allowing for multiple headers with the same name.
> +The header is added to successful responses
> +(2xx and 3xx status codes) by default.
> +.It Ic remove Ar name
> +Suppress the HTTP response header with the specified
> +.Ar name .
> +This can be used to remove headers added by default, such as
> +.Qq Last-Modified ,
> +as well as headers inherited from a parent server configuration.
> +.El
> +.Pp
> +If
> +.Ic always
> +is specified, the header will be included in all responses,
> +including error pages (4xx and 5xx status codes).
> +.Pp
> +Header names are limited to 127 characters and values to 511 characters.
> +Headers defined in a
> +.Ic location
> +block are inherited from the
> +.Ic server
> +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 .
> +
>  .It Ic hsts Oo Ar option Oc
>  Enable HTTP Strict Transport Security.
>  Valid options are:
> @@ -895,6 +946,32 @@ server "example.com" {
>  	}
>  }
>  .Ed
> +.Pp
> +Custom HTTP headers can be added to responses for security or compatibility:
> +.Bd -literal -offset indent
> +server "example.com" {
> +	listen on * tls port 443
> +
> +	header set "X-Powered-By" "OpenBSD httpd"
> +
> +	header set "X-Content-Type-Options" "nosniff" always
> +	header set "X-Frame-Options" "DENY" always
> +
> +	header add "Set-Cookie" "id=23; HttpOnly; Secure"
> +
> +	# Remove default Last-Modified header
> +	header remove "Last-Modified"
> +
> +	location "/api/*" {
> +		# This location defines its own headers
> +		header add "Access-Control-Allow-Origin" "*"
> +		# Override from server context
> +		header set "Set-Cookie" "id=5; HttpOnly; Secure"
> +		# Remove from server context
> +		header remove "X-Frame-Options"
> +	}
> +}
> +.Ed
>  .Sh SEE ALSO
>  .Xr htpasswd 1 ,
>  .Xr glob 7 ,
> diff --git a/usr.sbin/httpd/httpd.h b/usr.sbin/httpd/httpd.h
> index baf11d1b523..096f68f02b0 100644
> --- a/usr.sbin/httpd/httpd.h
> +++ b/usr.sbin/httpd/httpd.h
> @@ -209,6 +209,7 @@ enum imsg_type {
>  	IMSG_CFG_MEDIA,
>  	IMSG_CFG_AUTH,
>  	IMSG_CFG_FCGI,
> +	IMSG_CFG_HEADERS,
>  	IMSG_CFG_DONE,
>  	IMSG_LOG_ACCESS,
>  	IMSG_LOG_ERROR,
> @@ -470,6 +471,19 @@ struct fastcgi_param {
>  };
>  TAILQ_HEAD(server_fcgiparams, fastcgi_param);
>  
> +struct custom_header {
> +	char			name[128];
> +	char			value[512];
> +	uint8_t			flags;
> +#define HEADER_REMOVE		0x01
> +#define HEADER_ADD		0x02
> +#define HEADER_SET		0x04
> +#define HEADER_ALWAYS		0x08
> +
> +	TAILQ_ENTRY(custom_header) entry;
> +};
> +TAILQ_HEAD(server_headers, custom_header);
> +
>  struct server_config {
>  	uint32_t		 id;
>  	uint32_t		 parent_id;
> @@ -540,6 +554,7 @@ struct server_config {
>  
>  	struct server_fcgiparams fcgiparams;
>  	int			 fcgistrip;
> +	struct server_headers	 headers;
>  	char			 errdocroot[HTTPD_ERRDOCROOT_MAX];
>  
>  	TAILQ_ENTRY(server_config) entry;
> @@ -673,6 +688,8 @@ void	 server_http(void);
>  int	 server_httpdesc_init(struct client *);
>  void	 server_read_http(struct bufferevent *, void *);
>  void	 server_abort_http(struct client *, unsigned int, const char *);
> +void	 server_custom_headers(struct server_config *, struct kvtree *,
> +	    unsigned int);
>  unsigned int
>  	 server_httpmethod_byname(const char *);
>  const char
> @@ -825,9 +842,12 @@ int	 config_getcfg(struct httpd *, struct imsg *);
>  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 *);
> +void	 config_inherit_headers(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_headers(struct httpd *, struct imsg *);
>  int	 config_setmedia(struct httpd *, struct media_type *);
>  int	 config_getmedia(struct httpd *, struct imsg *);
>  int	 config_setauth(struct httpd *, struct auth *);
> diff --git a/usr.sbin/httpd/parse.y b/usr.sbin/httpd/parse.y
> index 326b249662f..d2d6282439f 100644
> --- a/usr.sbin/httpd/parse.y
> +++ b/usr.sbin/httpd/parse.y
> @@ -142,6 +142,7 @@ typedef struct {
>  %token	ERROR INCLUDE AUTHENTICATE WITH BLOCK DROP RETURN PASS REWRITE
>  %token	CA CLIENT CRL OPTIONAL PARAM FORWARDED FOUND NOT
>  %token	ERRDOCS GZIPSTATIC BANNER
> +%token	HEADER ADD ALWAYS REMOVE SET
>  %token	<v.string>	STRING
>  %token  <v.number>	NUMBER
>  %type	<v.port>	port
> @@ -323,6 +324,7 @@ server		: SERVER optmatch STRING	{
>  			SPLAY_INIT(&srv->srv_clients);
>  			TAILQ_INIT(&srv->srv_hosts);
>  			TAILQ_INIT(&srv_conf->fcgiparams);
> +			TAILQ_INIT(&srv_conf->headers);
>  
>  			TAILQ_INSERT_TAIL(&srv->srv_hosts, srv_conf, entry);
>  		} '{' optnl serveropts_l '}'	{
> @@ -557,6 +559,7 @@ serveroptsl	: LISTEN ON STRING opttls port	{
>  		| root
>  		| directory
>  		| banner
> +		| header
>  		| logformat
>  		| fastcgi
>  		| authenticate
> @@ -643,6 +646,7 @@ serveroptsl	: LISTEN ON STRING opttls port	{
>  			srv = s;
>  			srv_conf = &srv->srv_conf;
>  			SPLAY_INIT(&srv->srv_clients);
> +			TAILQ_INIT(&srv_conf->headers);
>  		} '{' optnl serveropts_l '}'	{
>  			struct server	*s = NULL;
>  			uint32_t	 f;
> @@ -710,6 +714,147 @@ banner		: BANNER		{
>  		}
>  		;
>  
> +header		: HEADER REMOVE STRING	{
> +			struct custom_header	*hdr;
> +
> +			if ((hdr = calloc(1, sizeof(*hdr))) == NULL)
> +				fatal("out of memory");
> +
> +			if (strlcpy(hdr->name, $3, sizeof(hdr->name)) >=
> +			    sizeof(hdr->name)) {
> +				yyerror("header name truncated");
> +				free($3);
> +				free(hdr);
> +				YYERROR;
> +			}
> +			if (strcmp("Server", hdr->name) == 0) {
> +				yyerror("'header remover Server' "
> +					"ignored, use 'no banner'");
> +				free($3);
> +				free(hdr);
> +				YYERROR;
> +			}
> +			free($3);
> +
> +			hdr->flags = HEADER_REMOVE;
> +			TAILQ_INSERT_TAIL(&srv->srv_conf.headers, hdr, entry);
> +		}
> +		| HEADER ADD STRING STRING	{
> +			struct custom_header	*hdr;
> +
> +			if ((hdr = calloc(1, sizeof(*hdr))) == NULL)
> +				fatal("out of memory");
> +
> +			if (strlcpy(hdr->name, $3, sizeof(hdr->name)) >=
> +			    sizeof(hdr->name)) {
> +				yyerror("header name truncated");
> +				free($3);
> +				free($4);
> +				free(hdr);
> +				YYERROR;
> +			}
> +			free($3);
> +
> +			if (strlcpy(hdr->value, $4, sizeof(hdr->value)) >=
> +			    sizeof(hdr->value)) {
> +				yyerror("header value truncated");
> +				free($4);
> +				free(hdr);
> +				YYERROR;
> +			}
> +			free($4);
> +
> +			hdr->flags = HEADER_ADD;
> +			TAILQ_INSERT_TAIL(&srv->srv_conf.headers, hdr, entry);
> +		}
> +		| HEADER ADD STRING STRING ALWAYS	{
> +			struct custom_header	*hdr;
> +
> +			if ((hdr = calloc(1, sizeof(*hdr))) == NULL)
> +				fatal("out of memory");
> +
> +			if (strlcpy(hdr->name, $3, sizeof(hdr->name)) >=
> +			    sizeof(hdr->name)) {
> +				yyerror("header name truncated");
> +				free($3);
> +				free($4);
> +				free(hdr);
> +				YYERROR;
> +			}
> +			free($3);
> +
> +			if (strlcpy(hdr->value, $4, sizeof(hdr->value)) >=
> +			    sizeof(hdr->value)) {
> +				yyerror("header value truncated");
> +				free($4);
> +				free(hdr);
> +				YYERROR;
> +			}
> +			free($4);
> +
> +			hdr->flags = HEADER_ADD;
> +			hdr->flags |= HEADER_ALWAYS;
> +			TAILQ_INSERT_TAIL(&srv->srv_conf.headers, hdr, entry);
> +		}
> +		| HEADER SET STRING STRING	{
> +			struct custom_header	*hdr;
> +
> +			if ((hdr = calloc(1, sizeof(*hdr))) == NULL)
> +				fatal("out of memory");
> +
> +			if (strlcpy(hdr->name, $3, sizeof(hdr->name)) >=
> +			    sizeof(hdr->name)) {
> +				yyerror("header name truncated");
> +				free($3);
> +				free($4);
> +				free(hdr);
> +				YYERROR;
> +			}
> +			free($3);
> +
> +			if (strlcpy(hdr->value, $4, sizeof(hdr->value)) >=
> +			    sizeof(hdr->value)) {
> +				yyerror("header value truncated");
> +				free($4);
> +				free(hdr);
> +				YYERROR;
> +			}
> +			free($4);
> +
> +			hdr->flags = HEADER_SET;
> +			TAILQ_INSERT_TAIL(&srv->srv_conf.headers, hdr, entry);
> +		}
> +		| HEADER SET STRING STRING ALWAYS	{
> +			struct custom_header	*hdr;
> +
> +			if ((hdr = calloc(1, sizeof(*hdr))) == NULL)
> +				fatal("out of memory");
> +
> +			if (strlcpy(hdr->name, $3, sizeof(hdr->name)) >=
> +			    sizeof(hdr->name)) {
> +				yyerror("header name truncated");
> +				free($3);
> +				free($4);
> +				free(hdr);
> +				YYERROR;
> +			}
> +			free($3);
> +
> +			if (strlcpy(hdr->value, $4, sizeof(hdr->value)) >=
> +			    sizeof(hdr->value)) {
> +				yyerror("header value truncated");
> +				free($4);
> +				free(hdr);
> +				YYERROR;
> +			}
> +			free($4);
> +
> +			hdr->flags = HEADER_SET;
> +			hdr->flags |= HEADER_ALWAYS;
> +			TAILQ_INSERT_TAIL(&srv->srv_conf.headers, hdr, entry);
> +		}
> +		;
> +
>  optfound	: /* empty */	{ $$ = 0; }
>  		| FOUND		{ $$ = 1; }
>  		| NOT FOUND	{ $$ = -1; }
> @@ -1447,7 +1592,9 @@ lookup(char *s)
>  	/* this has to be sorted always */
>  	static const struct keywords keywords[] = {
>  		{ "access",		ACCESS },
> +		{ "add",		ADD },
>  		{ "alias",		ALIAS },
> +		{ "always",		ALWAYS },
>  		{ "authenticate",	AUTHENTICATE},
>  		{ "auto",		AUTO },
>  		{ "backlog",		BACKLOG },
> @@ -1475,6 +1622,7 @@ lookup(char *s)
>  		{ "forwarded",		FORWARDED },
>  		{ "found",		FOUND },
>  		{ "gzip-static",	GZIPSTATIC },
> +		{ "header",		HEADER },
>  		{ "hsts",		HSTS },
>  		{ "include",		INCLUDE },
>  		{ "index",		INDEX },
> @@ -1500,6 +1648,7 @@ lookup(char *s)
>  		{ "prefork",		PREFORK },
>  		{ "preload",		PRELOAD },
>  		{ "protocols",		PROTOCOLS },
> +		{ "remove",		REMOVE },
>  		{ "request",		REQUEST },
>  		{ "requests",		REQUESTS },
>  		{ "return",		RETURN },
> @@ -1507,6 +1656,7 @@ lookup(char *s)
>  		{ "root",		ROOT },
>  		{ "sack",		SACK },
>  		{ "server",		SERVER },
> +		{ "set",		SET },
>  		{ "socket",		SOCKET },
>  		{ "strip",		STRIP },
>  		{ "style",		STYLE },
> @@ -2298,12 +2448,24 @@ server_inherit(struct server *src, struct server_config *alias,
>      struct server_config *addr)
>  {
>  	struct server	*dst, *s, *dstl;
> +	struct custom_header	*hdr, *nhdr;
>  
>  	if ((dst = calloc(1, sizeof(*dst))) == NULL)
>  		fatal("out of memory");
>  
>  	/* Copy the source server and assign a new Id */
>  	memcpy(&dst->srv_conf, &src->srv_conf, sizeof(dst->srv_conf));
> +
> +	TAILQ_INIT(&dst->srv_conf.headers);
> +	TAILQ_FOREACH(hdr, &src->srv_conf.headers, entry) {
> +		if ((nhdr = calloc(1, sizeof(*nhdr))) == NULL)
> +			fatal("out of memory");
> +		strlcpy(nhdr->name, hdr->name, sizeof(nhdr->name));
> +		strlcpy(nhdr->value, hdr->value, sizeof(nhdr->value));
> +		nhdr->flags = hdr->flags;
> +		TAILQ_INSERT_TAIL(&dst->srv_conf.headers, nhdr, entry);
> +	}
> +
>  	if ((dst->srv_conf.tls_cert_file =
>  	    strdup(src->srv_conf.tls_cert_file)) == NULL)
>  		fatal("out of memory");
> @@ -2393,6 +2555,18 @@ server_inherit(struct server *src, struct server_config *alias,
>  			fatal("out of memory");
>  
>  		memcpy(&dstl->srv_conf, &s->srv_conf, sizeof(dstl->srv_conf));
> +
> +		/* Copy custom headers from source location */
> +		TAILQ_INIT(&dstl->srv_conf.headers);
> +		TAILQ_FOREACH(hdr, &s->srv_conf.headers, entry) {
> +			if ((nhdr = calloc(1, sizeof(*nhdr))) == NULL)
> +				fatal("out of memory");
> +			strlcpy(nhdr->name, hdr->name, sizeof(nhdr->name));
> +			strlcpy(nhdr->value, hdr->value, sizeof(nhdr->value));
> +			nhdr->flags = hdr->flags;
> +			TAILQ_INSERT_TAIL(&dstl->srv_conf.headers, nhdr, entry);
> +		}
> +
>  		strlcpy(dstl->srv_conf.name, alias->name,
>  		    sizeof(dstl->srv_conf.name));
>  
> diff --git a/usr.sbin/httpd/server.c b/usr.sbin/httpd/server.c
> index a38cf018d81..ed97f3f7e3c 100644
> --- a/usr.sbin/httpd/server.c
> +++ b/usr.sbin/httpd/server.c
> @@ -470,6 +470,7 @@ void
>  serverconfig_free(struct server_config *srv_conf)
>  {
>  	struct fastcgi_param	*param, *tparam;
> +	struct custom_header	*hdr, *thdr;
>  
>  	free(srv_conf->return_uri);
>  	free(srv_conf->tls_ca_file);
> @@ -485,6 +486,9 @@ serverconfig_free(struct server_config *srv_conf)
>  
>  	TAILQ_FOREACH_SAFE(param, &srv_conf->fcgiparams, entry, tparam)
>  		free(param);
> +
> +	TAILQ_FOREACH_SAFE(hdr, &srv_conf->headers, entry, thdr)
> +		free(hdr);
>  }
>  
>  void
> @@ -503,6 +507,7 @@ serverconfig_reset(struct server_config *srv_conf)
>  	srv_conf->tls_ocsp_staple = NULL;
>  	srv_conf->tls_ocsp_staple_file = NULL;
>  	TAILQ_INIT(&srv_conf->fcgiparams);
> +	TAILQ_INIT(&srv_conf->headers);
>  }
>  
>  struct server *
> @@ -1366,6 +1371,9 @@ server_dispatch_parent(int fd, struct privsep_proc *p, struct imsg *imsg)
>  	case IMSG_CFG_FCGI:
>  		config_getserver_fcgiparams(httpd_env, imsg);
>  		break;
> +	case IMSG_CFG_HEADERS:
> +		config_getserver_headers(httpd_env, imsg);
> +		break;
>  	case IMSG_CFG_DONE:
>  		config_getcfg(httpd_env, imsg);
>  		break;
> diff --git a/usr.sbin/httpd/server_fcgi.c b/usr.sbin/httpd/server_fcgi.c
> index c5f9917204c..02165ba3e32 100644
> --- a/usr.sbin/httpd/server_fcgi.c
> +++ b/usr.sbin/httpd/server_fcgi.c
> @@ -723,6 +723,8 @@ server_fcgi_header(struct client *clt, unsigned int code)
>  			return (-1);
>  	}
>  
> +	server_custom_headers(srv_conf, &resp->http_headers, code);
> +
>  	/* Date header is mandatory and should be added as late as possible */
>  	key.kv_key = "Date";
>  	if (kv_find(&resp->http_headers, &key) == NULL &&
> diff --git a/usr.sbin/httpd/server_http.c b/usr.sbin/httpd/server_http.c
> index afdb73f243f..96faa0fb4e5 100644
> --- a/usr.sbin/httpd/server_http.c
> +++ b/usr.sbin/httpd/server_http.c
> @@ -50,10 +50,12 @@ void		 server_httpdesc_free(struct http_descriptor *);
>  int		 server_http_authenticate(struct server_config *,
>  		    struct client *);
>  static int	 http_version_num(char *);
> +static int	 http_is_success(unsigned int code);
>  char		*server_expand_http(struct client *, const char *,
>  		    char *, size_t);
>  char		*replace_var(char *, const char *, const char *);
>  char		*read_errdoc(const char *, const char *);
> +char		*get_always_custom_headers(struct server_config *);
>  
>  static struct http_method	 http_methods[] = HTTP_METHODS;
>  static struct http_error	 http_errors[] = HTTP_ERRORS;
> @@ -219,6 +221,25 @@ http_version_num(char *version)
>  	}
>  	return (0);
>  }
> +static int
> +http_is_success(unsigned int code)
> +{
> +	switch (code) {
> +	case 200:
> +	case 201:
> +	case 204:
> +	case 206:
> +	case 301:
> +	case 302:
> +	case 303:
> +	case 304:
> +	case 307:
> +	case 308:
> +		return (0);
> +	default:
> +		return (1);
> +	}
> +}
>  
>  void
>  server_read_http(struct bufferevent *bev, void *arg)
> @@ -892,6 +913,7 @@ server_abort_http(struct client *clt, unsigned int code, const char *msg)
>  	char			 tmbuf[32], hbuf[128], *hstsheader = NULL;
>  	char			*clenheader = NULL;
>  	char			*bannerheader = NULL, *bannertoken = NULL;
> +	char			*customheaders = NULL;
>  	char			 buf[IBUF_READ_SIZE];
>  	char			*escapedmsg = NULL;
>  	char			 cstr[5];
> @@ -1060,6 +1082,8 @@ server_abort_http(struct client *clt, unsigned int code, const char *msg)
>  			goto done;
>  		}
>  
> +	customheaders = get_always_custom_headers(srv_conf);
> +
>  	/* Add basic HTTP headers */
>  	if (asprintf(&httpmsg,
>  	    "HTTP/1.0 %03d %s\r\n"
> @@ -1070,6 +1094,7 @@ server_abort_http(struct client *clt, unsigned int code, const char *msg)
>  	    "%s"
>  	    "%s"
>  	    "%s"
> +	    "%s"
>  	    "\r\n"
>  	    "%s",
>  	    code, httperr, tmbuf,
> @@ -1077,6 +1102,7 @@ server_abort_http(struct client *clt, unsigned int code, const char *msg)
>  	    clenheader == NULL ? "" : clenheader,
>  	    extraheader == NULL ? "" : extraheader,
>  	    hstsheader == NULL ? "" : hstsheader,
> +	    customheaders == NULL ? "" : customheaders,
>  	    desc->http_method == HTTP_METHOD_HEAD || clenheader == NULL ?
>  	    "" : body) == -1)
>  		goto done;
> @@ -1092,6 +1118,7 @@ server_abort_http(struct client *clt, unsigned int code, const char *msg)
>  	free(clenheader);
>  	free(bannerheader);
>  	free(bannertoken);
> +	free(customheaders);
>  	if (msg == NULL)
>  		msg = "\"\"";
>  	if (asprintf(&httpmsg, "%s (%03d %s)", msg, code, httperr) == -1) {
> @@ -1561,6 +1588,73 @@ server_locationaccesstest(struct server_config *srv_conf, const char *path)
>  	    (ret == 0 && SRVFLAG_LOCATION_NOT_FOUND & srv_conf->flags));
>  }
>  
> +void
> +server_custom_headers(struct server_config *srv_conf, struct kvtree *headers,
> +    unsigned int code)
> +{
> +	struct custom_header	*hdr;
> +	struct kv		*kv, search;
> +
> +	TAILQ_FOREACH(hdr, &srv_conf->headers, entry) {
> +		/* Only include headers not marked ALWAYS on success. */
> +		if (!(hdr->flags & HEADER_ALWAYS) &&
> +		    http_is_success(code) != 0)
> +			continue;
> +
> +		search.kv_key = hdr->name;
> +
> +		/* deletes all existing headers of the same key */
> +		if (hdr->flags & HEADER_REMOVE) {
> +			while ((kv = kv_find(headers, &search)) != NULL)
> +				kv_delete(headers, kv);
> +		/* replaces all existing headers of the same name */
> +		} else if (hdr->flags & HEADER_SET) {
> +			while ((kv = kv_find(headers, &search)) != NULL)
> +				kv_delete(headers, kv);
> +
> +			if (kv_add(headers, hdr->name, hdr->value) == NULL)
> +				return;
> +		/* appends a new header without checking for duplicates */
> +		} else if (hdr->flags & HEADER_ADD) {
> +			if (kv_add(headers, hdr->name, hdr->value) == NULL)
> +				return;
> +		}
> +	}
> +}
> +
> +/*
> + * Build a raw custom HTTP header that only includes headers marked as always
> + */
> +char *
> +get_always_custom_headers(struct server_config *srv_conf)
> +{
> +	struct custom_header *hdr;
> +	char *headers = NULL;
> +	char *tmp = NULL;
> +
> +	TAILQ_FOREACH(hdr, &srv_conf->headers, entry) {
> +		if (!(hdr->flags & HEADER_ALWAYS)
> +		      || hdr->flags & HEADER_REMOVE)
> +			continue;
> +
> +		if (headers == NULL) {
> +			if (asprintf(&headers, "%s: %s\r\n", hdr->name,
> +			    hdr->value) == -1) {
> +				return (NULL);
> +			}
> +		} else {
> +			if (asprintf(&tmp, "%s%s: %s\r\n", headers, hdr->name,
> +			    hdr->value) == -1) {
> +				free(headers);
> +				return (NULL);
> +			}
> +			free(headers);
> +			headers = tmp;
> +		}
> +	}
> +	return (headers);
> +}
> +
>  int
>  server_response_http(struct client *clt, unsigned int code,
>      struct media_type *media, off_t size, time_t mtime)
> @@ -1627,6 +1721,7 @@ server_response_http(struct client *clt, unsigned int code,
>  		    "; preload" : "") == -1)
>  			return (-1);
>  	}
> +	server_custom_headers(srv_conf, &resp->http_headers, code);
>  
>  	/* Date header is mandatory and should be added as late as possible */
>  	if (server_http_time(time(NULL), tmbuf, sizeof(tmbuf)) <= 0 ||
>