Skip to content

Commit 121afa3

Browse files
committed
Add haveged to image-builder's rootfs.
haveged ensures that there is sufficient entropy available to processes running within the microvm. Without sufficient entropy, it is fairly easy for processes to get blocked on reading /dev/random or making getrandom() syscalls, including during boot (which can result in CreateVM calls to fail if the agent process gets blocked). haveged was chosen as it enforces no minimum kernel requirements, does not add CPU requirements (i.e. existence of RDRAND or similar instructions) and is currently used by Debian for related use cases such as seeding entropy in their installer. One other option was "rngd", which has versions that support use of RDRAND. It was not chosen as RDRAND is not universally trusted or portable. Similarly, use of the "random.trust_cpu=on" kernel boot parameter was ruled out for now as it relies on RDRAND and additionally enforces a minimum kernel version of 4.19. Signed-off-by: Erik Sipsma <[email protected]>
1 parent ea15dbf commit 121afa3

File tree

6 files changed

+150
-2
lines changed

6 files changed

+150
-2
lines changed

runtime/service_integ_test.go

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1104,3 +1104,116 @@ func TestUpdateVMMetadata_Isolated(t *testing.T) {
11041104
t.Logf("stdout output from task %q: %s", containerName, stdout)
11051105
assert.Equalf(t, "45", stdout, "container %q did not emit expected stdout", containerName)
11061106
}
1107+
1108+
// TestRandomness validates that there is a reasonable amount of entropy available to the VM and thus
1109+
// randomness available to containers (test reads about 2.5MB from /dev/random w/ an overall test
1110+
// timeout of 60 seconds). It also validates that the quality of the randomness passes the rngtest
1111+
// utility's suite.
1112+
func TestRandomness_Isolated(t *testing.T) {
1113+
prepareIntegTest(t)
1114+
1115+
ctx, cancel := context.WithTimeout(namespaces.WithNamespace(context.Background(), defaultNamespace), 60*time.Second)
1116+
defer cancel()
1117+
1118+
client, err := containerd.New(containerdSockPath, containerd.WithDefaultRuntime(firecrackerRuntime))
1119+
require.NoError(t, err, "unable to create client to containerd service at %s, is containerd running?", containerdSockPath)
1120+
defer client.Close()
1121+
1122+
image, err := alpineImage(ctx, client, defaultSnapshotterName())
1123+
require.NoError(t, err, "failed to get alpine image")
1124+
containerName := "test-entropy"
1125+
1126+
const blockCount = 1024
1127+
ddContainer, err := client.NewContainer(ctx,
1128+
containerName,
1129+
containerd.WithSnapshotter(defaultSnapshotterName()),
1130+
containerd.WithNewSnapshot("test-entropy-snapshot", image),
1131+
containerd.WithNewSpec(
1132+
oci.WithDefaultUnixDevices,
1133+
// Use blocksize of 2500 as rngtest consumes data in blocks of 2500 bytes.
1134+
oci.WithProcessArgs("/bin/dd", "iflag=fullblock", "if=/dev/random", "of=/dev/stdout", "bs=2500",
1135+
fmt.Sprintf("count=%d", blockCount)),
1136+
),
1137+
)
1138+
require.NoError(t, err, "failed to create container %s", containerName)
1139+
1140+
// rngtest is a utility to "check the randomness of data using FIPS 140-2 tests", installed as part of
1141+
// the container image this test is running in. We pipe the output from "dd if=/dev/random" to rngtest
1142+
// to validate the quality of the randomness inside the VM.
1143+
// TODO It would be conceptually simpler to just run rngtest inside the container in the VM, but
1144+
// doing so would require some updates to our test infrastructure to support custom-built container
1145+
// images running in VMs (right now it's only feasible to use publicly available container images).
1146+
// Right now, it's instead run as a subprocess of this test outside the VM.
1147+
var rngtestStdout bytes.Buffer
1148+
var rngtestStderr bytes.Buffer
1149+
rngtestCmd := exec.CommandContext(ctx, "rngtest",
1150+
// we set this to 1 less than the number of blocks read by dd above to account for the fact that
1151+
// the first 32 bits read by rngtest are not used for the tests themselves
1152+
fmt.Sprintf("--blockcount=%d", blockCount-1),
1153+
)
1154+
rngtestCmd.Stdout = &rngtestStdout
1155+
rngtestCmd.Stderr = &rngtestStderr
1156+
rngtestStdin, err := rngtestCmd.StdinPipe()
1157+
require.NoError(t, err, "failed to get pipe to rngtest command's stdin")
1158+
1159+
ddStdout := rngtestStdin
1160+
var ddStderr bytes.Buffer
1161+
1162+
task, err := ddContainer.NewTask(ctx, cio.NewCreator(cio.WithStreams(nil, ddStdout, &ddStderr)))
1163+
require.NoError(t, err, "failed to create task for dd container")
1164+
1165+
exitCh, err := task.Wait(ctx)
1166+
require.NoError(t, err, "failed to wait on task for dd container")
1167+
1168+
err = task.Start(ctx)
1169+
require.NoError(t, err, "failed to start task for dd container")
1170+
1171+
err = rngtestCmd.Start()
1172+
require.NoError(t, err, "failed to start rngtest")
1173+
1174+
select {
1175+
case exitStatus := <-exitCh:
1176+
assert.NoError(t, exitStatus.Error(), "failed to retrieve exitStatus")
1177+
assert.EqualValues(t, 0, exitStatus.ExitCode())
1178+
1179+
status, err := task.Delete(ctx)
1180+
assert.NoErrorf(t, err, "failed to delete dd task after exit")
1181+
if status != nil {
1182+
assert.NoError(t, status.Error())
1183+
}
1184+
1185+
t.Logf("stderr output from dd:\n %s", ddStderr.String())
1186+
case <-ctx.Done():
1187+
require.Fail(t, "context cancelled",
1188+
"context cancelled while waiting for dd container to exit (is it blocked on reading /dev/random?), err: %v", ctx.Err())
1189+
}
1190+
1191+
err = rngtestCmd.Wait()
1192+
t.Logf("stdout output from rngtest:\n %s", rngtestStdout.String())
1193+
t.Logf("stderr output from rngtest:\n %s", rngtestStderr.String())
1194+
if err != nil {
1195+
// rngtest will exit non-zero if any blocks fail its randomness tests.
1196+
// Trials showed an approximate false-negative rate of 27/32863 blocks,
1197+
// so testing on 1023 blocks gives a ~36% chance of there being a single
1198+
// false-negative. The chance of there being 5 or more drops down to
1199+
// about 0.1%, which is an acceptable flakiness rate, so we assert
1200+
// that there are no more than 4 failed blocks.
1201+
// Even though we have a failure tolerance, the test still provides some
1202+
// value in that we can be aware if a change to the rootfs results in a
1203+
// regression.
1204+
require.EqualValues(t, 1, rngtestCmd.ProcessState.ExitCode())
1205+
const failureTolerance = 4
1206+
1207+
for _, outputLine := range strings.Split(rngtestStderr.String(), "\n") {
1208+
var failureCount int
1209+
_, err := fmt.Sscanf(strings.TrimSpace(outputLine), "rngtest: FIPS 140-2 failures: %d", &failureCount)
1210+
if err == nil {
1211+
if failureCount > failureTolerance {
1212+
require.Failf(t, "too many d block test failures from rngtest",
1213+
"%d failures is greater than tolerance of up to %d failures", failureCount, failureTolerance)
1214+
}
1215+
break
1216+
}
1217+
}
1218+
}
1219+
}

tools/docker/Dockerfile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,8 @@ RUN apt-get update && apt-get install --yes --no-install-recommends \
103103
iptables \
104104
iperf3 \
105105
libdevmapper-dev \
106-
libseccomp-dev
106+
libseccomp-dev \
107+
rng-tools # used for rngtest
107108

108109
RUN mkdir -p /var/lib/firecracker-containerd/runtime \
109110
&& curl --silent --show-error --retry 3 --max-time 30 --output default-vmlinux.bin \

tools/image-builder/Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ ifneq ($(UID),0)
5555
endif
5656
debootstrap \
5757
--variant=minbase \
58-
--include=udev,systemd,systemd-sysv,procps,libseccomp2 \
58+
--include=udev,systemd,systemd-sysv,procps,libseccomp2,haveged \
5959
stretch \
6060
"$(WORKDIR)" $(DEBMIRROR)
6161
rm -rf "$(WORKDIR)/var/cache/apt/archives" \

tools/image-builder/README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,14 @@ the final parameter passed on the kernel command line.
7474
A complete command line, settable via the `kernel_args` setting in `/etc/containerd/firecracker-runtime.json`, is:
7575

7676
ro console=ttyS0 noapic reboot=k panic=1 pci=off nomodules systemd.journald.forward_to_console systemd.unit=firecracker.target init=/sbin/overlay-init
77+
78+
### Security ###
79+
80+
In order to ensure sufficient entropy is consistently available within
81+
the VM, the rootfs is configured to start the
82+
[`haveged`](https://manpages.debian.org/buster/haveged/haveged.8.en.html)
83+
daemon during boot. [More information on its method of operation and other
84+
details can be found in its FAQ](https://issihosts.com/haveged/faq.html).
85+
Users of the image created by this utility are encouraged to evaluate
86+
`haveged` against their security requirements before running any
87+
cryptographically-sensitive workloads inside their microVMs and containers.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/etc/systemd/system/haveged.service
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
[Unit]
2+
Description=Entropy Daemon based on the HAVEGE algorithm
3+
Documentation=man:haveged(8) http://www.issihosts.com/haveged/
4+
DefaultDependencies=no
5+
ConditionVirtualization=!container
6+
After=local-fs.target
7+
Before=firecracker.target sysinit.target shutdown.target
8+
9+
[Service]
10+
ExecStart=/usr/sbin/haveged --Foreground --verbose=1 -w 1024
11+
SuccessExitStatus=143
12+
SecureBits=noroot-locked
13+
NoNewPrivileges=yes
14+
CapabilityBoundingSet=CAP_SYS_ADMIN
15+
PrivateTmp=yes
16+
PrivateDevices=yes
17+
PrivateNetwork=yes
18+
ProtectSystem=full
19+
ProtectHome=yes
20+
21+
[Install]
22+
WantedBy=default.target

0 commit comments

Comments
 (0)