sandbox

Building a File Server by Subtraction

A rough record of reducing a default Ubuntu server into a more limited machine for file serving and log collection, guided by security-brutalist ideas about purpose, subtraction, and survivability.

This project was influenced by The Security Brutalist, especially its insistence that systems should be judged by purpose, constraint, and survivability rather than aesthetic notions of security. What follows is not an attempt to restate those ideas as my own. It is a record of trying to apply them, imperfectly, to a small Ubuntu file server in a home lab.

Why Ubuntu

Ubuntu was chosen deliberately.

Part of the reason was practical. It is common, widely deployed, and used to provide real services in real environments. Part of the reason was methodological. Ubuntu arrives with a broad set of defaults intended to make many kinds of hardware and many kinds of workflows just work. That makes it a useful system to examine if the goal is to understand how convenience, compatibility, and inherited functionality translate into attack surface.

It also acted as a deliberate constraint. In real IT environments, one rarely gets to shut everything down, start again from first principles, and rebuild on an ideal hardened base. More often, one enters an already existing ecosystem and has to make the best use of what is already in the stack. Starting from Ubuntu mirrored that condition. The task was not to design a perfect server from nothing. The task was to take a broadly compatible general-purpose system and reduce it into something smaller, more legible, and more defensible.

Objective

The machine was not meant to be a general-purpose Linux box. Its role was limited.

It needed to:

  • serve files to the systems meant to access them
  • ingest and collect logs from endpoints on the home network
  • remain accessible for administration under controlled conditions

From that point on, everything on the machine could be asked the same question: what do you do here, and what happens if I remove you?

If a service did not support file serving, remote administration, or observability, it had to justify its existence or be removed.

Method

To stop the project from becoming vague hardening theatre, I audited services one by one.

The structure was simple:

  • purpose
  • dependencies
  • risk introduced
  • risk reduced
  • kill impact
  • verdict
  • immediate actions

This forced a distinction between presence and necessity. Ubuntu shipped with many things that made sense in a broad operating system. That did not automatically make them appropriate in an internal server with a small brief.

Subtraction became the governing move. Some services were removed entirely. Some were kept, but only after being constrained more tightly. In both cases, the goal was the same: preserve the few functions that mattered and reduce everything else.

What subtraction meant

Some of the easiest decisions were also the most revealing. Services such as PolicyKit, ModemManager, and NetworkManager were not malicious or poorly designed. They simply reflected a broader intended use case than the one this machine needed to serve.

That is an important distinction. The point was not that Ubuntu defaults are wrong. The point was that broad defaults become dangerous when they remain unexamined inside a machine with a limited role.

If the machine was meant to live on the internal network as a file server and log collector, then mobile broadband management, desktop-oriented privilege brokering, and flexible network convenience layers had to justify themselves against that role. In these cases, they could not.

Break and fix: least privilege is easy to say, harder to implement

One of the first places the project stopped being abstract was service identity.

Creating a dedicated nologin service user is straightforward. Understanding how that decision interacts with systemd unit files, unit locations, service precedence, capabilities, and existing defaults is less straightforward. I found that out most clearly while trying to tighten the service model around flow collection.

The difficult part was not creating the account. The difficult part was understanding how to make the system actually use the new arrangement instead of falling back to the old one. That produced the familiar Linux frustration of doing real work and ending up exactly where you started because the wrong unit was still winning or the old settings were still live.

The softflowd side of this made the lesson plain. The intended end state was simple enough on paper: run the exporter under a dedicated softflow identity, constrain it with CapabilityBoundingSet=CAP_NET_RAW, set NoNewPrivileges=yes, and tighten the service with ProtectSystem=strict and ProtectHome=yes. What took longer was learning that service hardening is not just a list of directives. It is also a question of where the unit lives, which unit systemd is actually reading, and how old assumptions survive longer than expected.

That friction turned out to be useful. It exposed the difference between having a principle and having an implementation. Least privilege on paper is easy. Least privilege that actually survives contact with a running system is something else.

Break and fix: rebuilding NetFlow capture on tighter terms

The server's observability role meant that flow collection was worth keeping. It also meant that the default trust model around that service had to be examined.

For nfcapd, the working path was to move away from the stock arrangement and rebuild the service on tighter terms. A dedicated netflow user was created with /usr/sbin/nologin. The stock service was then stopped, disabled, and masked, any lingering process was killed, and a custom unit under /etc/systemd/system/ took its place. After systemctl daemon-reload, the new service was enabled and started explicitly, checked with systemctl status, and then the unit file itself was locked with chattr +i.

The custom unit was plain about its priorities: run as netflow, write to /var/cache/nfdump, restart on failure, clear the capability bounding set, and tighten the service with NoNewPrivileges=yes, PrivateTmp=yes, ProtectSystem=strict, MemoryMax=256M, and TasksMax=50.

That sequence was instructive because it showed what subtraction meant in practice. A useful service was not kept simply because it already existed. It had to be rebuilt so that its continued existence matched the role of the machine more closely.

Break and fix: immutability without procedure becomes fragility

Another lesson came from trying to preserve configuration integrity with chattr and audit rules.

Immutability is attractive in principle. It promises resistance to drift, accidental change, and tampering. But strong controls still need an operating model. In firewall management, that became obvious quickly. Freezing the wrong files does not create meaningful security. It simply blocks legitimate administration and produces self-inflicted failure.

The useful pattern turned out to be procedural rather than absolute: thaw, change, verify, refreeze, audit. In practice that meant understanding the difference between /etc/ufw/*.rules, which could be treated as relatively static templates, and /var/lib/ufw/user*.rules, which are where CLI edits actually land. Freezing the latter too aggressively means ufw allow ... stops being an administrative tool and starts returning refusal.

The same lesson appeared in the audit tooling. Audit rules had to live in one place, be validated with augenrules --check, loaded with augenrules --load, and then locked. Duplicate or overlapping rules only made the system noisier and harder to reason about. The lesson was not to abandon immutability, but to understand where it belonged and how it should be maintained.

Break and fix: removing convenience without losing function

There is a difference between deleting services and reducing a system.

Deleting services is easy. Reducing a system means removing things while preserving the core function of the machine. That is why removing network convenience layers mattered. NetworkManager and ModemManager existed to support flexibility across changing hardware and network conditions. In a static internal file server, that flexibility was less valuable than legibility.

The pattern here was blunt but disciplined: stop, disable, mask, purge, verify. In the audit notes that verification was not abstract. It included checking whether the service was still enabled and confirming the server still had working network access afterwards. The point was not to achieve ideological purity. The point was to confirm that the server still served files, still remained reachable under the intended conditions, and still retained the narrow functions it was built to perform.

Logging, visibility, and constraint

If the machine was to be in a better condition than its original state, observability had to be treated as part of its purpose rather than as an optional extra.

That meant keeping and hardening logging components, preserving useful telemetry, and thinking seriously about audit trails. It also introduced a constraint that could not be argued away: disk size.

One of the more useful ideas in the surrounding security-brutalist writing is that constraint is not an exception but the environment. That was true here. WORM-style or append-only handling was an attractive ideal, but this was a small home-lab box, not an infinite storage appliance. The question therefore became less pure and more practical: what can this machine preserve reliably over time without collapsing under the weight of its own logging?

That tension is still unresolved in the strict sense. But it is at least named, and once a constraint is named it can be worked with instead of ignored.

The rules in practice

Looking back, the project was an attempt to follow the rules, if not perfectly then at least concretely.

Rule 2 showed up in the insistence that every service answer to the machine's role. Rule 3 showed up in the repeated preference for subtraction and legibility over broad compatibility. Rule 5 showed up whenever storage, time, or the existing operating system limited what could be done. Rule 8 showed up whenever friction exposed a weak assumption. Rule 9 showed up in the simple fact that none of this counted until it was actually live on the machine. Rule 12 showed up in the repeated pattern of trying, breaking, reassessing, and rebuilding.

The value of the rules was not that they produced a perfect outcome. The value was that they provided a way to judge the machine without pretending that defaults are neutral or that hardening can be achieved through declarations alone.

Current state

The server now does what it is meant to do.

It serves files to the systems meant to read them. It ingests and collects logs from endpoints on the home network. Access is controlled through internal-network IP whitelisting, and the services that remain have been pushed closer to least-privilege operation than they were in the default state.

It is a more limited machine than the one Ubuntu first installed. More importantly, it is a more legible one.

Conclusion

This was not implemented perfectly.

It was implemented.

That matters. The machine is in a better state than it was when it began: less broad, less trusting, more deliberate, and more aligned to the small set of functions it is actually supposed to perform.

But that improvement should not be mistaken for completion. The survivability of the server does not come from one round of reduction or one pass of hardening notes. It depends on consistent observability, maintenance, and discipline. A reduced system that is no longer watched will drift. A hardened service that is no longer understood will eventually become another black box.

So this is not a story about arriving at a final secure state. It is a record of making a common system better by narrowing it, understanding it, and accepting that keeping it in that state is an ongoing obligation rather than a one-time achievement.