Secure containerized SPA hosting with spartan
Every now and then I have to build some browser based application for which I often end up using an SPA (single page application) approach. For instance, combining Vite with React and React Router in my opinion is a fast approach to get something ready quick. Adding MUI on top for a nice-looking experience can help too.
Then comes the time when this needs to be hosted somewhere, such that browsers
can download the static assets linked from the index.html at the root to make
the application availble for others to use. Using your everyday hosting provider
can be simple and straightforward, and very often a good enough choice. Those
providers usually have some offerings for static web hosting (which an SPA
basically is), which they build in their infrastructure through virtual hosts
through Apache HTTP Server or nginx or something like that. Those servers do
the trick, and they can be configured through - sometimes very complicated -
configuration settings. Sometimes I use this sort of thing too. So far so good.
I end up running more and more such applications on containerized infrastructure and in such a case, while they still work, Apache’s httpd, nginx and others in my opinion are not such a good choice anymore. Their pre-built container images come with a ton of stuff that I don’t need, let alone tons of libraries and other tools that each add more to an evergrowing list of dependencies that need to be kept up-to-date with respect to security patches and bug fixes.
This is where I thought to myself: why isn’t this more simple, more secure? When hosting publicly available static assets in a container, I don’t want or need shells, management tools, tons of configurable modules or settings. I just need the static assets, plus a minimal web server that is serving those assets with an understanding of SPA architecture.
Enter spartan
That’s why I built spartan, a simple and secure web server targeting hosting of SPAs through containers. Built with golang, the web server itself is small, does not have any dependencies on dynamically loaded libraries, and is uses default configuration that makes your everyday SPA secure, specifically also in terms of XSS and things like that.
The official spartan container images
contain one file (the statically linked spartan executable), apart from some
additional configuration like using user 1000 by default. Built from scratch,
the image today is <8MiB in size. Compare that to nginx (>50MiB) and httpd
(>60MiB). That is, even if you add SPA assets with a total of >40MiB to a
spartan base image, you’ll still end up with smaller container images, and
way smaller attack surfaces.
Secure by default
Above I briefly referred to spartan’s secure-by-default approach. While
configurable through YAML configuration, spartan applies default settings for
security-relevant response headers making the application you’re hosting secure.
Currently, the following response headers are automatically added:
- Content-Security-Policy (CSP) header
- Permissions-Policy header
- Referrer-Policy header
- Strict-Transport-Security header
- X-Content-Type-Options header
But many SPAs need additional resources like stylesheets, scripts, fonts, etc. that are loaded from external sources (CDNs, ad providers, telemetry providers, etc.) and for such cases, the configuration can be tailored to your exact needs.
For example, Vite allows to add placeholders for nonce valeus in the generated
index.html by configuring it with something like the following in a
vite.config.ts file:
export default defineConfig({
// ...
html: {
cspNonce: 'CSP_NONCE_PLACEHOLDER',
},
// ...
});
When bundling everything up for release, this will add references like the
following in the generated index.html:
<!-- ... --->
<script type="module" crossorigin src="/assets/index-CZftpWSK.js"
nonce="CSP_NONCE_PLACEHOLDER"></script>
<!-- ... --->
You can tell spartan that it should replace the CSP_NONCE_PLACEHOLDER in
the static assets with a new nonce value every time the content is served, and
it will happily oblige and inject the securly generated nonce values in the
response’s content-security-policy header as well as the output.
server:
# ...
security:
contentSecurityPolicy:
# ...
scriptSrc:
- nonce: CSP_NONCE_PLACEHOLDER
# ...
You can probably achieve the same thing with Apache HTTP Server or nginx, but
it certainly is a whole lot easier to do with spartan.
By the way, spartan uses the Apache-2.0 license, its sources are all on
github.com/rokeller/spartan, and it is
actively worked on/maintained. Go check it out!