Home
Unchained
Security Blog

The principle of immutability

Matt Moore, CTO

TL;DR 


Immutability is not about stopping change — it is about controlling change — and immutability by default is an important component of being secure by design.


If there were a hill I was going to die on, it just might be this one. I am an immutability die hard. But by advocating for immutability it makes it sound like I am averse to change, which couldn’t be further from the truth (see the principle of ephemerality). So how do we reconcile a love for immutability and constant change? Immutability is about controlling change. It is about being intentional about change.


Immutability by default is a key facet of being secure by default. Let’s take a look at some examples.


Programming languages


Let’s start with one of the spicier topics for software developers: the programming language.


The vast majority of programming languages have the concept of a constant; even bash has a way to declare variables as readonly! The more I advance in my career, the more appreciation I have for Functional Programming (in which virtually every variable is read only). While I haven’t seriously worked in a functional language in decades, some of the most successful libraries I have helped build and maintain present functional interfaces. The states are immutable, but you can apply mutations to produce new states.


However, I spent the first decade of my professional career writing C++, mostly on a C++ compiler team. I would absolutely consider myself a fan of C++, but the mutability of variables by default means you have to add const to designate immutability, something I call “necessary knobs”.


class State {
public:
  const Value& lookup(const Key& arg) const;
}

Ok, now sit down and take a deep breath because I don’t want to pick a fight … This is a place where Rust improves upon C++. (gasp!) C++ could never realistically change the default mutability of variables to const, the billions of lines or so of legacy code it would break make such a change prohibitive. So, Rust choosing to make variables immutable by default gives every Rust program a safer starting point. In Rust you are required to be intentional about when you want things to be mutable by using the mut keyword.


fn main() {
    let mut x = 5;
    println!("The value of x is: {x}");
    x = 6;
    println!("The value of x is: {x}");
}

When variable immutability is an opt-in “necessary knob” (like const), folks will rarely “turn” that knob (and a great many variables never actually change). I can’t tell you how many times I have stared at a piece of code assuming a non-const variable is mutated and searched for a mutation that doesn’t exist. Or conversely, badly assuming that a non-const variable is const only to uncover a mutation later after I have introduced some bug. I’ve never had either of these experiences with Rust or functional languages.


Package managers


A topic that goes hand-in-hand with programming languages these days is the package manager (though this topic isn’t limited to language package managers). Most modern programming languages or build tools have a form of package manager they integrate with (e.g. npm, yarn, pip, bundler, maven, cargo, go modules, terraform). Effectively, every single language package manager has converged on the same pattern of using two files for an expression of dependencies: the human author-able constraints (e.g. package.json) and the locked constraint solution (e.g. package-lock.json).


By checking in these lock files you achieve immutability and your dependencies are generally reproducible. However, even having frozen things into this immutable state you can float dependencies forward intentionally with tools like dependabot:


Screenshot of floating dependencies forward intentionally with tools like dependabot.

One of the key benefits of immutability with intentional bumps by a tool like dependabot is that your version control clearly reflects the last known good state. So if you intentionally flow forward and things break, then you have the version control history to roll back to the prior immutable snapshot.

Screenshot of reverting back to the prior immutable snapshot.

If you are not pinning your package dependencies, then you are effectively allowing unreviewed third-party changes into your application without any recourse for rolling changes back.


Content addressing


The topic of version control makes a nice segue to the topic of “content addressing” because a few of the most popular modern version control systems (incl. git) are built around content addressing.

Content addressing is a technique where the name of a particular thing is itself a checksum of its content. This is how the SHA1 commit hashes in Git work, the commit hashes (in red) become the names of immutable points in time along a branch (in blue):


Screenshot of how the SHA1 commit hashes in Git work, the commit hashes (in red) become the names of immutable points in time along a branch (in blue).

This technique is fascinating because when you are referencing something via its content address you are cryptographically guaranteed to get back what you wanted (if it is available), since it is trivially audited.


Another one of my favorite content addressing use cases is of course the OCI image. While most folks typically reference an OCI image by tag, the tag (like a git branch) is just a mutable label on a checksum called a “digest” (in this case SHA256). This three-part (1, 2, 3) series talks through our general philosophy on tags (in blue) and digests (in red):


Screenshot of an OCI image by tag.

One of the other powerful applications of content addressability is that it gives a really natural checksum for use in signing. The Sigstore project has applied this technique to both Git (with gitsign) and OCI images (with cosign).


Similar to package managers, you can use tools like dependabot and digestabot to “have your cake and eat it too,” or in the words of Muhammad Ali:


Meme of Muhammad Ali with text: float like a tag, sting like a digest — Muhammad Ali.

For example, in contexts like GitHub Actions where best practice is to pin actions to a particular commit, you can configure dependabot to send pull requests floating these commit hashes forward as the upstream action releases new versions:


Screenshot of pull requests.

We use digestabot to let us do the same thing for picking up new digests from a tag:

Screenshot of picking up new digests from a tag.

This pinning to a content address that can float forwards over time allows you to be very intentional about picking up changes to workflows, base images, and deployments. It enables you to qualify updates with presubmit testing before breaking changes are merged. If problems are uncovered after the update is merged, it gives a clear mechanism for rolling back. From a security standpoint, this practice also keeps an upstream Git or OCI repository from being able to silently change the behavior without a downstream change to review and merge the update.


Infrastructure


The topic of OCI images makes for an excellent segue to the topic of immutable infrastructure because the practices behind immutable infrastructure (while certainly not unique to containers) are the de facto way virtually every container platform supports deploying them.


The core idea behind immutable infrastructure is that once a resource is deployed, it isn’t changed. When there’s a new version to be deployed, new resources are provisioned that replace the old version, and the old version is then torn down. Immutable infrastructure is great from the perspective of ephemerality because each instance of an application is replaced each time a deployment occurs.


Knowing that a deployed resource shouldn’t change also has a number of follow-on security benefits. When in-place updates aren’t performed:


  • a significant portion of the root filesystem can be made read-only (more immutability by default).


  • the scope of binaries to monitor for runtime threat detection becomes fixed.


  • the need for running things as root at runtime drops significantly (a win for least privilege).


  • the need for things like package managers in the final image effectively goes away enabling “distroless”-style minimalism.


The security benefits of immutable infrastructure are immense, and Chainguard Images are purpose built for these kinds of immutable infrastructure use cases.


Conclusion


The idea of not embracing immutability reminds me a lot of the Mario franchise’s Boos: the ghosts that only chase you when you are looking away, but freeze when you are facing them. Likewise when something like a dependency is not pinned, you relinquish any control of when it changes, and Murphy’s Law stipulates that this will break you when you least expect it. Immutability is about having control over change. It’s the opposite of Boo. Things only change when you intend them to, and remain frozen when you are not.


The principle of immutability also reminds me a lot of our Chainguard company value of having a bias for intentional action. We value a willingness to take action, but folks must be intentional about what actions are taken when, and how they go about it. The principle of immutability isn’t about stopping change, it is about being intentional about it.


If you want to learn more about how Chainguard Images can help your projects or organization, please reach out to our team!

Share

Ready to Lock Down Your Supply Chain?

Talk to our customer obsessed, community-driven team.

Get Started