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 encodedtrusted:trueto trust the certificate (falseexplicitly 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
| Approach | Best for | Boot overhead | Limitation |
|---|---|---|---|
User data (settings.pki) | One or a few CAs | None | 16 KB user data limit |
| Bootstrap container + apiclient | Large bundles stored in S3 | About 2s (slim image) to 15s (official image) | Extra moving parts |
| Custom AMI | Fleets that value speed and reliability | Near zero | You 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.