unrolled/secure: security headers as one Go middleware and one config struct
HTTP middleware for Go that facilitates some quick security wins.
At a glance
- What is it?
- A small net/http handler that adds HSTS, frame options, content type sniffing, CSP with per request nonces and the cross origin isolation headers, configured entirely through a struct.
- Who is it for?
- This library does the boring thirty minutes well. It is a single `net/http.Handler` wrapper, so it drops in front of anything, and the part worth understanding rather than copying is the `$NONCE` substitution, which turns a static CSP string into a per request policy that actually blocks injected scripts.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 159 days ago.
- What is it written in?
- Mainly Go, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 7, 2026, and from our analysis. They are not legal advice.
Editorial analysis
It is a handler, so it fits anything
The README's opening line gives the shape away: Secure is an HTTP middleware for Go that facilitates some quick security wins. It is a standard `net/http` Handler and can be used with many frameworks or directly with Go's own net/http package.
That constraint is the design. Because it depends on nothing but `net/http`, there is no framework adapter to adopt, no interface to satisfy, and no dependency to audit. The `go.mod` file is as thin as it gets: module path and a `go 1.13` directive, with no requirements at all. The repository tree matches, with `secure.go`, `csp.go` and their tests, plus a `cspbuilder/` directory for the content security policy helpers.
The usage example is the ordinary Go pattern of wrapping a handler:
var myHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hello world"))
})
func main() {
secureMiddleware := secure.New(secure.Options{
AllowedHosts: []string{"ssl.example.com"},
SSLRedirect: true,
SSLHost: "ssl.example.com",
FrameDeny: true,
})
app := secureMiddleware.Handler(myHandler)
http.ListenAndServe("127.0.0.1:3000", app)
}There is one ordering instruction in the README that matters more than the rest: put the middleware as close to the top of the chain as possible, but after logging and recovery. The reason is stated too, that the allowed hosts and SSL check should happen before anything else, so a rejected request never reaches your application logic.
Nonces are the feature that makes the CSP work
Content security policy is the option people adopt this library for, and the README describes a detail that most wrappers skip. Passing a template string will replace `$NONCE` with a dynamic nonce value of 16 bytes for each request, which can be retrieved later using the `Nonce` function.
ContentSecurityPolicy: "script-src $NONCE",A static CSP with `script-src 'self'` or `script-src 'unsafe-inline'` does very little, because inline scripts are how most XSS payloads run. A nonce based policy is the practical modern answer: the server generates a random value per response, puts it in the header, and only scripts carrying that exact value execute. The library generates the value and hands it to you, so the remaining work is putting it on your script tags and style attributes.
The README shows the resulting headers, and it is worth reading them as a checklist of what you are actually turning on:
Strict-Transport-Security: 31536000; includeSubdomains; preload
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Content-Security-Policy: script-src 'nonce-a2ZobGFoZg=='Every default is off, which is deliberate and worth understanding. `FrameDeny`, `ContentTypeNosniff`, `BrowserXssFilter` and `SSLRedirect` all default to false, and `STSSeconds` defaults to 0, which means the HSTS header is not included at all. Turning everything on with one struct is the intent, but you have to write the struct to get there, which is the right default for headers that can break an application if enabled blindly.
Options for the proxy reality behind every deployment
Roughly a third of the options exist because your Go process is almost never the thing that saw the client's HTTPS connection. There is a cluster of them and they are worth reading as one idea.
`SSLProxyHeaders` is a map of header keys to values that indicate the original request was HTTPS, with the README suggesting the Nginx equivalent of `map[string]string{"X-Forwarded-Proto": "https"}`. `HostsProxyHeaders` is the same idea for the hostname, naming `X-Forwarded-Host` in the example. `SSLHost` sets the hostname used when redirecting HTTP to HTTPS, and `SSLHostFunc` does the same job with a function, taking precedence when it is set. `AllowRequestFunc` hands you the request and returns whether it should proceed, for logic the other options cannot express.
The host allow list has a matching detail: `AllowedHosts` takes fully qualified domain names, and `AllowedHostsAreRegex` tells the middleware whether to treat them as patterns. The usage example uses `example\.com` and `.*\.example\.com` with that flag on, which is how you allow a domain and all of its subdomains in one list.
On HSTS there is `ForceSTSHeader`, which exists for the case where the header should always be sent even though the connection as seen by this process is not HTTPS, which is exactly what happens behind a TLS terminating proxy. `STSPreload` adds the `preload` flag, and that one has a real operational consequence: submitting a domain to the browser preload list is nearly irreversible, so treat it as a decision about the whole domain rather than a config flag.
Set IsDevelopment in development or you will fight it
The README has a short section with an emphatic title about setting the `IsDevelopment` option to true when developing, and the reason it deserves the emphasis is that it disables three checks at once.
When `IsDevelopment` is true, `AllowedHosts`, `SSLRedirect` and the HSTS header are not in effect. In practice that means you can work over plain HTTP without being redirected to HTTPS, and `localhost` will not be blocked as a bad host. Without it, a local development server will redirect every request to an HTTPS hostname that does not resolve, and you will spend time wondering why nothing loads.
`ForceSTSHeader` respects this override too, which is consistent: development mode means no HTTPS assumptions regardless of how you configure the rest.
The rest of the option list covers the modern header set, and it is long enough to be worth skimming for names you may not know. `ReferrerPolicy`, `PermissionsPolicy`, `CrossOriginOpenerPolicy` and `CrossOriginEmbedderPolicy` are all there, and `FeaturePolicy` is present with a note that it is deprecated and renamed to PermissionsPolicy, so you can tell which one to reach for. `CustomFrameOptionsValue`, `CustomBrowserXssValue` and `AllowRequestFunc` exist as escape hatches when the simple boolean is not enough.
One more option is about correctness rather than headers. `SSLTemporaryRedirect` switches the HTTP to HTTPS redirect from the default 301 to a 307, which matters because browsers cache 301 aggressively, so you may not want a permanent redirect recorded from a staging setup. Version v1.16.0 included a fix to a comment that named the wrong status code for this option, a small sign that the option list is read closely.
The `cspbuilder/` directory in the tree suggests policy construction is factored out separately from the handler, which is where you would look if you need a policy assembled from parts rather than passed as one string.
Two years between the last release and the last push
The repository record has a gap worth naming. The most recent tagged release is v1.17.0 from 2024-10-22, and GitHub reports the last push on 2026-05-01. So the code has moved in the last eighteen months while the published versions stopped almost two years ago.
What v1.17.0 actually changed is informative. It adds COEP, COOP, `X-DNS-Prefetch-Control` and `X-Permitted-Cross-Domain-Policies` as options, contributed from outside the maintainer, and alongside it there is a chore whose stated goal is to ensure the package does not add any headers. Those two changes in the same release are the crux of the design philosophy: this library adds headers when you ask and stays out of the way otherwise. A contributor adding four new headers was immediately followed by a maintainer tightening the default to add nothing.
That means if you are on v1.17.0 you do not have the cross origin isolation options, which arrived in that same release, so check what your version actually supports before configuring them. The version before it, v1.16.0, is a good illustration of the maintenance style: a comment fix, a readme comment fix, marshalling `Options` in json, yaml and toml while skipping func fields, a linter cleanup, and adding Go 1.23 to the test matrix. Small, correct, unremarkable changes.
The project is MIT licensed with 2,357 stars, 144 forks and zero open issues, which is a healthy ratio. The `Makefile` shows the local checks: `golangci-lint run ./...` plus `go test -v -cover -race -count=1 ./...` and `go vet`, with a separate lighter `ci` target that skips the linter for speed.
Editorial conclusion
This library does the boring thirty minutes well. It is a single `net/http.Handler` wrapper, so it drops in front of anything, and the part worth understanding rather than copying is the `$NONCE` substitution, which turns a static CSP string into a per request policy that actually blocks injected scripts. Wire it in early in your middleware chain, set `IsDevelopment: true` in local builds so the HTTPS redirect and host allow list get out of the way, and enable `ForceSTSHeader` if you terminate TLS somewhere this middleware cannot see. Read the options list once and turn on only the headers whose behavior you can explain.
Frequently asked questions
What does unrolled/secure do in Go?
It is HTTP middleware that adds security response headers to any net/http handler, including HSTS, X-Frame-Options, X-Content-Type-Options, Content-Security-Policy and the cross origin isolation headers. It depends on nothing outside the standard library.
How do I use a nonce with the ContentSecurityPolicy option?
Pass a template string containing $NONCE, for example ContentSecurityPolicy: "script-src $NONCE". Secure generates a random 16 byte nonce per request, substitutes it into the header, and exposes it through the Nonce function so you can add it to your script tags.
Why should I set IsDevelopment to true locally?
When IsDevelopment is true the AllowedHosts check, the SSL redirect and the HSTS header are all skipped. Without it, local HTTP requests get redirected to an HTTPS host that does not resolve and localhost is treated as an invalid host, which breaks development servers.
Which Go version does unrolled/secure support?
The go.mod declares go 1.13 and the module has no external requirements, so it can be dropped into almost any Go project. Version v1.16.0 added Go 1.23 to the test matrix, so newer toolchains are covered by CI.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/unrolled-secure)