How to Add a Custom CA Certificate to Bottlerocket Nodes on EKS

Bottlerocket is a lean, secure, container-focused OS from AWS. There’s no package manager, no SSH by default, and most of the filesystem is read-only. That’s great for security, but it raises a practical question the first time you hit a TLS error: how do you trust your own CA on a node you can’t really “log into”?

This post covers the defaults, why you’d need a custom CA, and two ways to install one.

1. Default certificates: where they live and how to check

Bottlerocket ships with a standard public CA bundle, much like any Linux distro. The OS builds the final trust store at boot and writes it to:

/etc/pki/tls/certs/ca-bundle.crt

This file is generated. Don’t try to edit it by hand, because Bottlerocket rebuilds it from its settings, and the root filesystem is read-only anyway.

To inspect it, connect to the node (SSM Session Manager into the control container), then hop into the admin container:

bash

[ssm-user@control]$ enter-admin-container
[root@admin]# sheltie

# Check the bundle
bash-5.2# ls -l /etc/pki/tls/certs/
bash-5.2# grep -c "BEGIN CERTIFICATE" /etc/pki/tls/certs/ca-bundle.crt

To see which custom CAs are configured through the API:

bash

apiclient get settings.pki

On a fresh node this returns nothing, which means only the default public CAs are trusted.

2. Why would you need a custom CA?

The classic case: kubelet needs to pull an image from a private registry that uses a certificate signed by your internal CA. The node doesn’t know that CA, so the pull fails with something like this:

Failed to pull image "registry.internal.example.com/app:1.0":
... tls: failed to verify certificate: x509: certificate signed by unknown authority

Your pod sits in ImagePullBackOff, and nothing you change in the Deployment will fix it, because the problem is on the node.

Other common reasons include:

  • A corporate proxy that intercepts TLS traffic.
  • Internal artifact repositories, Helm repos, or APIs that host-level components talk to.
  • Compliance rules requiring a private PKI across the whole stack.

One important note: settings.pki updates the host trust store, which covers containerd, kubelet, and host containers. It does not magically update the trust store inside your application pods. If your apps also need the CA, handle that separately (mounted bundles, a custom base image, and so on).

3. How to install a custom CA

Bottlerocket manages everything through its settings API, and CA certificates live under settings.pki. Each entry takes two fields:

  • data: the PEM certificate, base64 encoded
  • trusted: true to trust the certificate (false explicitly distrusts it)

First, base64 encode your PEM file as a single line:

bash

base64 -w0 my-root-ca.pem

Option A: User data (simple and fast)

If your certificate is small, put it directly into the node’s user data (TOML):

toml

[settings.pki.my-root-ca]
data = "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0t..."
trusted = true

With Karpenter, this goes into the userData field of your EC2NodeClass. With managed node groups, it goes in the launch template.

Why this is the best option when it fits: the CA is in place before kubelet starts, so the very first image pull already works. No extra containers, no extra boot time, no moving parts.

The catch: EC2 user data has a 16 KB limit. A single root CA fits easily, but a large corporate bundle with dozens or hundreds of certificates will not.

Option B: Bootstrap container + apiclient (for large bundles)

When your bundle is too big for user data, keep it somewhere like S3 and fetch it at boot using a bootstrap container. Bootstrap containers run once, early in the boot process, before kubelet starts. Bottlerocket mounts the host’s apiclient into them, so your script can update settings directly.

The user data just defines the bootstrap container:

toml

[settings.bootstrap-containers.fetch-ca]
source = "<your-registry>/bottlerocket-bootstrap:latest"
mode = "once"
essential = true
user-data = "<base64-encoded-script>"

And the script (simplified) does the real work:

bash

#!/bin/bash
set -euo pipefail

aws s3 cp s3://my-cert-bucket/certs/ca-bundle.pem /tmp/ca-bundle.pem

apiclient set \
  settings.pki.my-root-ca.data="$(base64 -w0 /tmp/ca-bundle.pem)" \
  settings.pki.my-root-ca.trusted=true

apiclient get settings.pki.my-root-ca

The node’s IAM role needs s3:GetObject on that bucket.

A few lessons we learned the hard way:

“Argument list too long.” With a very large bundle, passing the whole base64 blob to apiclient in one argument can fail. The fix is to split the bundle into individual certificates and set each one as its own entry (my-root-ca-1, my-root-ca-2, and so on). Put them all in one apiclient set call rather than looping, which is noticeably faster.

essential = true vs false. With true, the node fails to boot if the CA can’t be fetched (for example, S3 is unreachable or IAM is broken). That’s loud, but safe. With false, the node boots anyway without the CA, and you find out later when image pulls start failing. For most teams, failing loudly is the better choice.

Watch the boot time. Bootstrap containers aren’t free. In our testing, the official bootstrap image (around 139 MB, mostly the AWS CLI) added roughly 15 seconds to node boot. Surprisingly, the certificate logic itself took well under a second; nearly all the time went to unpacking the image and tearing down the container. Switching to a slim Alpine-based image (around 10 MB) that fetches from S3 with curl brought the bootstrap step down to about 2 seconds. If you autoscale aggressively, that difference really adds up.

Troubleshooting. When something goes wrong, check the bootstrap container’s logs from the admin container:

bash

journalctl -u "bootstrap-containers@fetch-ca"
apiclient get settings.pki

4. What about a custom AMI?

There’s a third option: bake the certificates straight into a custom Bottlerocket AMI.

The idea is simple. If the CA is already in the image, there’s nothing to fetch at boot. No S3 dependency, no bootstrap container, no user data size limit. In our early tests, certificate delivery went from about 2 seconds on the boot critical path to a few milliseconds, entirely off the critical path. It’s also the most reliable approach, because it can’t fail due to the network.

The trade-off is ownership: you maintain an image build pipeline and rebuild for every Bottlerocket release.

We’ll cover the full custom AMI build in an upcoming post, so stay tuned.

Summary

ApproachBest forBoot overheadLimitation
User data (settings.pki)One or a few CAsNone16 KB user data limit
Bootstrap container + apiclientLarge bundles stored in S3About 2s (slim image) to 15s (official image)Extra moving parts
Custom AMIFleets that value speed and reliabilityNear zeroYou maintain the image

Start with user data if your CA fits. Move to a bootstrap container when it doesn’t. And if boot speed or reliability really matters, the custom AMI is where you’ll end up.

,

Post navigation

Arunlal A

Engineer. Linux enthusiast. Traveller. DevOps learner and practitioner. I enjoy working with Linux, DevOps, automation, and technology while exploring new places along the way. I use this space to share my experiences, learnings, experiments, and things I discover along the journey. Whether you're a seasoned DevOps professional or just getting started, let's connect, learn, and grow together. Happy coding, automating, and deploying!

Leave a Reply

Your email address will not be published. Required fields are marked *