This is the multi-page printable view of this section.
Click here to print.
Return to the regular view of this page.
Protected Storage
Options for confidential storage
By default, CoCo workloads execute in confidential guest memory.
Files written to the workload filesystem will be stored in guest memory
and protected by confidential computing.
Of course, cloud native workloads can leverage a wide variety of external storage options.
Some of these might break the confidential trust model. Some can be used with adaptations
to the workload (e.g. wrapping secrets before storing them).
Carefully consider the trust model of any external storage volumes, services, or paradigms
before attaching them to a confidential workload.
To simplify things, Confidential Containers provides some confidential storage primitives.
1 - Confidential EmptyDir
Protected ephemeral storage for workloads
When using a confidential runtime class, all emptyDir volumes
will automatically be created on top of secure block devices.
These confidential emptyDir volumes use LUKS2 on top of a block device provided by the host.
Confidential emptyDir can be a good fit for a workload that needs to write
a lot of secret data to a scratch directory.
If stored inside the guest, this data could deplete guest memory.
Instead, the confidential emptyDir is backed by a block device provided by the host.
The block device is encrypted inside the guest such that the host cannot access the data.
Warning
While confidential emptyDir volumes have integrity protection via LUKS2,
they do not have replay protection.
While the host cannot modify individual blocks, they can rollback the entire
volume to some earlier state.
A confidential emptyDir can be added to a workload the same way a traditional
emptyDir would be used.
volumeMounts:
- name: scratch-volume
mountPath: /scratch-directory
volumes:
- name: scratch-volume
emptyDir:
sizeLimit: 64Gi
On the host, this volume will be backed by a sparse file. Data blocks are allocated as the volume
is used, but creating the filesystem, encryption, and integrity metadata consumes some physical
host storage immediately.
Sizing block-backed emptyDir volumes
Kata Containers does not currently use emptyDir.sizeLimit to size the block device presented to
the guest. Instead, it sets the maximum size of the sparse backing image, and therefore the logical
capacity of the guest block device, to the capacity of the host filesystem containing the volume.
Kubelet still counts the physically allocated blocks against sizeLimit.
With the current ext4 formatting options, initial filesystem metadata uses approximately 0.08% of
the logical capacity (about 2.5 GB for 2.9 TB). Encryption and integrity added about 50 MB in the
same test.
On a large host filesystem, the initial metadata allocation can consume a significant part of a
small sizeLimit, reducing the space available to the workload or causing the pod to be evicted
before it writes data. Allow enough headroom in sizeLimit for this metadata, or place kubelet’s
volume data on a smaller dedicated filesystem. See the Kata Containers
storage limitations
for current overhead estimates, implementation details, and tracked improvements.
Note
LUKS2 does not support discard/trim when integrity is enabled.
Thus, when a file is removed from the emptyDir by the guest,
the size of the sparse file will not decrease.
Over the lifespan of the workload, the size of the sparse file
can only increase.
Confidential emptyDir volumes are ephemeral. They are removed when the pod is torn down.
The LUKS2 header for the volume is stored in guest memory and is not accessible to the host.
If you want to use an emptyDir that isn’t backed by a LUKS volume,
set the emptyDir medium to Memory. This will create an emptyDir
that is stored in guest memory.
volumeMounts:
- name: memory-empty-vol
mountPath: "/tmp/cache"
volumes:
- name: memory-empty-vol
emptyDir:
medium: Memory
sizeLimit: "50M"
2 - Confidential Container Image Storage
Experimental: Use protected ephemeral storage instead of confidential guest memory for guest-pulled image layers
With guest image management,
container images are pulled and unpacked inside the confidential guest.
Like other files written to the workload filesystem, their layers are stored
in confidential guest memory by default. Specifically, the layers are written
to /run/kata-containers/image, which is backed by guest memory. Large images
can therefore significantly increase a pod’s memory requirements.
This experimental storage feature moves these layers to a host-provided block
device.
The Kata agent asks the Confidential Data Hub (CDH) to encrypt and
integrity-protect the device inside the guest using dm-crypt and dm-integrity,
then mount it at /run/kata-containers/image.
The host provides the storage but cannot read or modify individual data blocks
without detection.
Warning
Confidential image storage is ephemeral and does not provide replay protection.
Both downloaded image layers and the OverlayFS upper and work directories for
the container writable layer are stored on the protected block device.
Provide a raw block PersistentVolumeClaim using a suitable storage driver and
expose it to a container at the reserved device path /dev/trusted_store. The
PersistentVolumeClaim must use volumeMode: Block. The Kata agent detects the
reserved device path and prepares the device for image-layer storage.
spec:
volumes:
- name: image-storage
persistentVolumeClaim:
claimName: image-storage-pvc
containers:
- name: app
volumeDevices:
- name: image-storage
devicePath: /dev/trusted_store
From the workload’s perspective, this storage is ephemeral: it is not an image
cache that can be reused by later pods. The lifecycle of the underlying
PersistentVolume still follows its Kubernetes reclaim policy. The feature
reduces confidential guest memory usage for container image layers.
See the Kata Containers guide for the complete
guest image management and block-volume setup.
Try it with local storage
For prototyping on a single node, a file on a disk-backed host filesystem can
be attached as a loop device and exposed through a local PersistentVolume.
This setup requires root access to the node and is not suitable for production.
Create a sparse 64 GiB backing file and attach it to an available loop device.
The host filesystem must have enough free space as the file grows:
NODE_NAME="$(kubectl get nodes -o jsonpath='{.items[0].metadata.name}')"
LOOP_FILE="/var/lib/coco/image-storage.img"
sudo mkdir -p "$(dirname "${LOOP_FILE}")"
sudo truncate --size 64G "${LOOP_FILE}"
LOOP_DEVICE="$(sudo losetup --find --show "${LOOP_FILE}")"
Create a StorageClass, local PersistentVolume, and raw block
PersistentVolumeClaim for that device:
cat <<EOF | kubectl apply -f -
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: local-image-storage
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: image-storage-pv
spec:
capacity:
storage: 64Gi
volumeMode: Block
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: local-image-storage
local:
path: ${LOOP_DEVICE}
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- ${NODE_NAME}
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: image-storage-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 64Gi
volumeMode: Block
storageClassName: local-image-storage
EOF
The PVC can remain Pending until the pod is created because the StorageClass
uses WaitForFirstConsumer. Attach the claim to an existing guest-pull workload
at /dev/trusted_store.
After deleting the workload, remove the storage objects, detach the loop
device, and remove the backing file:
kubectl delete pvc image-storage-pvc
kubectl delete pv image-storage-pv
kubectl delete storageclass local-image-storage
sudo losetup --detach "${LOOP_DEVICE}"
sudo rm -f "${LOOP_FILE}"