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:

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!