Security

For a long time now I’ve been worried about the lack of security in my dev environment.

Tools like brew make it absurdly easy to install third-party tools on your machine. On my laptop right now, I’ve got 304 formula and 30 casks installed, and my /opt/homebrew directory clocks in at 13G. Other package managers are no better: Nix brings you apparent immutability, but all the hashes in the world won’t tell you if something is safe to install, only that its content matches a digest[1]. No matter which package manager you use, you’re not auditing all of the packages you install, so you are in a sense delegating the task of monitoring the package registry to someone else; it could be a somewhat well-defined set of individuals, such as "the Debian maintainers", or something more nebulous, like "the watchers and contributors of the Homebrew repository".

And then there are the language ecosystems, each with its own package manager. Sampling a couple of small web apps on my machine, I find one[2] with 33,265 files in its node_modules directory and another[3] with 21,361. Note I said "small". These are not complex apps. They use React and number of related runtime and dev packages. Taking a non-web example that uses npm (well Yarn), even my dotfiles repo has 253 files in its node_modules, and that’s a project where I’ve tried to keep my JS dependencies to a bare minimum[4].

At the GUI level, I’m far less worried, although I recognize that it’s no longer true — if it ever was — that "you don’t have to worry about viruses and malware unless you’re on Windows". I spend most of my time in the terminal or in the browser, and the other apps I use — few in number — are either provided by Apple or some other megacorp (with the attendant security measures, which are imperfect but better than nothing), installed via the App Store (again, with some layer of filtering via the approval process, which presumably includes some kind of static analysis), or are from third-parties who have built up my trust over a period of time. None of this is bullet-proof, but the surface area is relatively limited, and it doesn’t feel like the Swiss-cheese command-line environment, with its barn-door-sized holes.

In the dev tooling space, I have felt cold comfort in knowing that pretty much the only thing between me and having my machine devastatingly compromised comes in the form of a "herd effect", where there is a kind of "safety" in numbers. Specifically, I’m relying on the fact that if there is a mass vulnerability propagated via a supply chain attack targeting some widely used dependency, it will probably be detected quickly (via the "many eyeballs" effect[5]), and I’ll be likely to hear about it soon, hopefully in such a way that I (or somebody positioned at a central choke point, such as working at a package registry) can mitigate it before it does me any harm.

I’m not sure who I heard say this, but it is an aphorism that security and convenience are always, always in tension with each other. You cannot have full security and utmost convenience at the same time. That is to say, you could air-gap your computer and audit literally every piece of source code you rely on before building and running it; this includes the kernel, the compiler, the libraries you use, the applications — the whole kit and kaboodle. But obviously, nobody does this, except perhaps for portions of the security apparatus of powerful nation states. And even for those, can they ever really be sure that somebody hasn’t inserted a self-concealing backdoor somewhere in the build chain? They would like to be, but I don’t think they can ever be (100% sure). So, we’re not going to go "all the way", sacrificing all convenience in the name of security. Common trade-offs include:

  • Storing your passwords and your 2FA tokens in the same password storage (eg. 1Password), in the name of convenience.
  • For that matter, storing all of your passwords in the single basket that is the cloud storage and closed-source application code of a for-profit proprietary software vendor (even if they publish a white paper on their security design).
  • Running closed-source software at all, or open-source software that you haven’t audited and that hasn’t been adequately engineered for security (ie. most of it, with notable exceptions).

One thing I want to explore is running more things in containers, which is easier now that Apple has rolled out a built-in container CLI. Now, containers are a pretty thin layer as far as security is concerned[6], but a thin-layer can still be useful in a defense-in-depth approach. The main benefit of containerization is reproducibility: you can specify what your dependencies are, prepare an image with those dependencies, and then run the same code in development and production. If you shift to another machine, you can take the container environment with you and everything will be the same. But while that is the main benefit, it’s not the only one. Installing a dependency in a container is a very different matter than "letting a third-party package shart bits of itself all over your base system". But this is again a security-vs-convenience trade-off: working with containers does involve another layer of abstraction, another set of tooling to learn, and another set of things to go wrong.

Now, it’s pretty clear that I could take those two web apps I mentioned above and containerize them fairly straightforwardly. Containers’ wheel-house, so to speak, is network services. If the ultimate goal of your service is to expose an app over HTTP listening on a port, then containers are a great fit: the container is a black box and you don’t need to care about what is going on inside it. But what about something like my dotfiles repo? I could certainly create a "base" dev image with all the stuff inside it that I need for a typical project — basically all or most of the stuff that I’m currently spraying all over the system with Homebrew — and then have pretty much all my terminal sessions start with me spinning up a container inside which I’ll do the real work.

But I’m not sure of what the details will be and whether it’s worth it for such a marginal additional layer of isolation. Will there be an unacceptable performance hit? Will updating the image be too much of a pain in the butt, even for a guy like me who is all too comfortable exploring the space between security and convenience? Will I still need to keep a bunch of third-party code running outside the container in order to get my daily work done? And if I end up giving the containers access to things like SSH and GPG keys, or Anthropic and other API tokens, won’t I just be back where I started, with a bunch of unaudited and untrusted third-party code running in an environment where it will have access to sensitive things that I would rather keep away from prying eyes and fingers?

All of these are open questions for now, but I’ll keep you posted if and when I actually start carrying out experiments in this direction[7]


  1. Reproducible builds are a related, useful idea, but while they close one potential gap, they are not a panacea. ↩︎

  2. Masochist. ↩︎

  3. I can’t link to this one as it’s private. ↩︎

  4. And while my npm footprint is small there, I have around 70 submodules in the repo, and the file count across those is over 7.5K. ↩︎

  5. "(1) with many eyes, shallow bugs get caught very quickly, and (2) that the more eyes there are, the more likely it is that some member of the group has sufficiently penetrating vision to catch the deeper-swimming bugs." ↩︎

  6. Attacks may exploit vulnerabilities that allow code inside a container, or within a VM, to break out and access, or otherwise affect, the host machine. And even in the absence of vulnerabilities, in order to be actually useful, containers often need access to network facilities and filesystem contents elsewhere. ↩︎

  7. If "keeping you posted" means pushing stuff to my dotfiles repo, that is. ↩︎