Skip to content

Commit 8e3e3c4

Browse files
authored
Merge pull request #326 from sipsma/haveged
Add haveged to image-builder's rootfs.
2 parents 1f8125e + 121afa3 commit 8e3e3c4

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)