From: Kirill A. Korinsky Subject: Re: add custom HTTP header support to httpd To: Rafael Sadowski Cc: tech@openbsd.org Date: Mon, 29 Dec 2025 23:49:44 +0100 On Fri, 26 Dec 2025 12:01:22 +0100, Rafael Sadowski wrote: > > Add custom HTTP header support to httpd > > Allow httpd.conf to add custom HTTP response headers or suppress > existing ones. This enables httpd to add security headers, custom > metadata, or remove unwanted headers without modifying app code. > > Three new directives are added: > > header add > Add custom header to successful responses (2xx, 3xx) > > header always add > Add custom header to all responses including errors > > header hide > Suppress a response header > > Headers can be specified in server blocks and are inherited by > location blocks unless the location defines its own headers. > > Example configuration: > > server "example.com" { > listen on * tls port 443 > > header always add "X-Frame-Options" "SAMEORIGIN" > header always add "X-Content-Type-Options" "nosniff" > header add "X-Powered-By" "OpenBSD httpd" > > location "/api/*" { > header add "Access-Control-Allow-Origin" "*" > } > } > > 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 > > Tests, feedback... welcome. I hadn't yet tested it, but I will, anyway it reads good and it triggers the first feedback: shall we backout "no banner" commit? It seems that header hide should be enough to make it in general way. > > Rafael > > diff --git a/usr.sbin/httpd/config.c b/usr.sbin/httpd/config.c > index b45081129b7..b115a1a8efa 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)); > @@ -281,6 +282,9 @@ config_setserver(struct httpd *env, struct server *srv) > > /* Configure FCGI parameters if necessary. */ > config_setserver_fcgiparams(env, srv); > + > + /* Configure custom headers if necessary. */ > + config_setserver_headers(env, srv); > } > } > > @@ -442,6 +446,104 @@ 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); > +} > + > +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) > { > @@ -640,6 +742,20 @@ config_getserver_config(struct httpd *env, struct server *srv, > > srv_conf->flags |= parent->flags & SRVFLAG_NO_BANNER; > > + /* Inherit custom headers from parent if location has none */ > + if (TAILQ_EMPTY(&srv_conf->headers)) { > + struct custom_header *hdr, *hdr_copy; > + TAILQ_FOREACH(hdr, &parent->headers, entry) { > + if ((hdr_copy = calloc(1, sizeof(*hdr_copy))) == NULL) > + goto fail; > + /* Copy only data fields, not TAILQ_ENTRY */ > + 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: %s %d location \"%s\", " > "parent \"%s[%u]\", flags: %s", > __func__, ps->ps_title[privsep_process], ps->ps_instance, > diff --git a/usr.sbin/httpd/httpd.conf.5 b/usr.sbin/httpd/httpd.conf.5 > index b3673284e0b..1b3cbea2504 100644 > --- a/usr.sbin/httpd/httpd.conf.5 > +++ b/usr.sbin/httpd/httpd.conf.5 > @@ -464,6 +464,32 @@ 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 header statements may be specified. > +Valid options are: > +.Bl -tag -width Ds > +.It Ic add Ar name Ar value > +Add a custom HTTP response header with the specified > +.Ar name > +and > +.Ar value . > +The header will be added to successful responses (2xx and 3xx status codes). > +.It Ic always add Ar name Ar value > +Add a custom HTTP response header that is included in all responses, > +including error responses (4xx and 5xx status codes). > +.It Ic hide Ar name > +Suppress an HTTP response header with the specified > +.Ar name . > +This can be used to remove headers that are added by default, > +such as > +.Qq Last-Modified > +or custom headers inherited from a parent server configuration. > +.El > +.Pp > +Header names are limited to 127 characters and values to 511 characters. > +Headers specified in a location block inherit headers from the parent > +server configuration unless the location defines its own headers. > .It Ic hsts Oo Ar option Oc > Enable HTTP Strict Transport Security. > Valid options are: > @@ -895,6 +921,29 @@ 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 > + > + # Add security headers to all responses > + header always add "X-Frame-Options" "SAMEORIGIN" > + header always add "X-Content-Type-Options" "nosniff" > + header always add "Content-Security-Policy" "default-src 'self'" > + > + # Add custom header to successful responses only > + header add "X-Powered-By" "OpenBSD httpd" > + > + # Hide default Last-Modified header > + header hide "Last-Modified" > + > + location "/api/*" { > + # Location inherits parent headers and can add more > + header add "Access-Control-Allow-Origin" "*" > + } > +} > +.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..a6dd2fb8a22 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,17 @@ struct fastcgi_param { > }; > TAILQ_HEAD(server_fcgiparams, fastcgi_param); > > +struct custom_header { > + char name[128]; > + char value[512]; > + uint8_t flags; > +#define HEADER_HIDE 0x01 > +#define HEADER_ALWAYS 0x02 > + > + TAILQ_ENTRY(custom_header) entry; > +}; > +TAILQ_HEAD(server_headers, custom_header); > + > struct server_config { > uint32_t id; > uint32_t parent_id; > @@ -540,6 +552,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 +686,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_add_custom_headers(struct server_config *, struct kvtree *, > + int, unsigned int); > unsigned int > server_httpmethod_byname(const char *); > const char > @@ -825,9 +840,11 @@ 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 *); > 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..f5fbd02e524 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 HIDE > %token STRING > %token NUMBER > %type 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,8 @@ serveroptsl : LISTEN ON STRING opttls port { > srv = s; > srv_conf = &srv->srv_conf; > SPLAY_INIT(&srv->srv_clients); > + TAILQ_INIT(&srv_conf->fcgiparams); > + TAILQ_INIT(&srv_conf->headers); > } '{' optnl serveropts_l '}' { > struct server *s = NULL; > uint32_t f; > @@ -710,6 +715,82 @@ banner : BANNER { > } > ; > > +header : HEADER HIDE 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; > + } > + free($3); > + > + hdr->flags = HEADER_HIDE; > + 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 = 0; > + TAILQ_INSERT_TAIL(&srv->srv_conf.headers, hdr, entry); > + } > + | HEADER ALWAYS ADD STRING STRING { > + struct custom_header *hdr; > + > + if ((hdr = calloc(1, sizeof(*hdr))) == NULL) > + fatal("out of memory"); > + > + if (strlcpy(hdr->name, $4, sizeof(hdr->name)) >= > + sizeof(hdr->name)) { > + yyerror("header name truncated"); > + free($4); > + free($5); > + free(hdr); > + YYERROR; > + } > + free($4); > + > + if (strlcpy(hdr->value, $5, sizeof(hdr->value)) >= > + sizeof(hdr->value)) { > + yyerror("header value truncated"); > + free($5); > + free(hdr); > + YYERROR; > + } > + free($5); > + > + hdr->flags = HEADER_ALWAYS; > + TAILQ_INSERT_TAIL(&srv->srv_conf.headers, hdr, entry); > + } > + ; > + > optfound : /* empty */ { $$ = 0; } > | FOUND { $$ = 1; } > | NOT FOUND { $$ = -1; } > @@ -1447,7 +1528,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 +1558,8 @@ lookup(char *s) > { "forwarded", FORWARDED }, > { "found", FOUND }, > { "gzip-static", GZIPSTATIC }, > + { "header", HEADER }, > + { "hide", HIDE }, > { "hsts", HSTS }, > { "include", INCLUDE }, > { "index", INDEX }, > diff --git a/usr.sbin/httpd/server.c b/usr.sbin/httpd/server.c > index 5d5063b6480..e63ae942035 100644 > --- a/usr.sbin/httpd/server.c > +++ b/usr.sbin/httpd/server.c > @@ -503,6 +503,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 * > @@ -1369,6 +1370,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 f3c01a459b0..b0e57194d39 100644 > --- a/usr.sbin/httpd/server_fcgi.c > +++ b/usr.sbin/httpd/server_fcgi.c > @@ -730,6 +730,8 @@ server_fcgi_header(struct client *clt, unsigned int code) > kv_add(&resp->http_headers, "Date", tmbuf) == NULL)) > return (-1); > > + server_add_custom_headers(srv_conf, &resp->http_headers, 0, code); > + > if (server_writeresponse_http(clt) == -1 || > server_bufferevent_print(clt, "\r\n") == -1 || > server_headers(clt, resp, server_writeheader_http, NULL) == -1 || > diff --git a/usr.sbin/httpd/server_http.c b/usr.sbin/httpd/server_http.c > index 485b67bc5e8..18a9fe2e1b3 100644 > --- a/usr.sbin/httpd/server_http.c > +++ b/usr.sbin/httpd/server_http.c > @@ -54,6 +54,7 @@ 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; > @@ -891,6 +892,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]; > @@ -1059,6 +1061,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" > @@ -1069,6 +1073,7 @@ server_abort_http(struct client *clt, unsigned int code, const char *msg) > "%s" > "%s" > "%s" > + "%s" > "\r\n" > "%s", > code, httperr, tmbuf, > @@ -1076,6 +1081,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; > @@ -1091,6 +1097,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) { > @@ -1560,6 +1567,76 @@ server_locationaccesstest(struct server_config *srv_conf, const char *path) > (ret == 0 && SRVFLAG_LOCATION_NOT_FOUND & srv_conf->flags)); > } > > +/* > + * Add or remove custom headers configured for this server. > + * Headers marked as always are included in all responses. > + * Regular headers are only added to 2xx and 3xx responses. > + */ > +void > +server_add_custom_headers(struct server_config *srv_conf, > + struct kvtree *headers, int is_error, unsigned int code) > +{ > + struct custom_header *hdr; > + struct kv *kv, search; > + > + TAILQ_FOREACH(hdr, &srv_conf->headers, entry) { > + if (is_error && !(hdr->flags & HEADER_ALWAYS)) > + continue; > + > + if (!is_error && !(hdr->flags & HEADER_ALWAYS)) { > + if (code != 200 && code != 201 && code != 204 && > + code != 206 && code != 301 && code != 302 && > + code != 303 && code != 304 && code != 307 && > + code != 308) > + continue; > + } > + > + if (hdr->flags & HEADER_HIDE) { > + /* Remove header from response */ > + search.kv_key = hdr->name; > + if ((kv = kv_find(headers, &search)) != NULL) > + kv_delete(headers, kv); > + } else { > + /* Add header to response */ > + kv_add(headers, hdr->name, hdr->value); > + } > + } > +} > + > +/* > + * Build a raw custom HTTP header. > + * only includes headers marked as always. > + * Returns the string or NULL on error/no headers. > + */ > +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_HIDE) > + 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 +1704,8 @@ server_response_http(struct client *clt, unsigned int code, > return (-1); > } > > + server_add_custom_headers(srv_conf, &resp->http_headers, 0, code); > + > /* Date header is mandatory and should be added as late as possible */ > if (server_http_time(time(NULL), tmbuf, sizeof(tmbuf)) <= 0 || > kv_add(&resp->http_headers, "Date", tmbuf) == NULL) > -- wbr, Kirill