Change for running as a non-root user (#276)

* Enable running container as mqm

* Fix merge problem

* Don't force root usage

* RHEL image runs as mqm instead of root

* Build on host with SELinux enabled

* Enable building on node in an OpenShift cluster

* Enable running container as mqm

* Fix merge problem

* Don't force root usage

* Merge lastest changes from master

* RHEL image runs as mqm instead of root

* Fix merge issues

* Test changes for non-root

* Make timeout properly, and more non-root test fixes

* Run tests with fewer/no capabilities

* Correct usage docs for non-root

* Add security docs

* Add temporary debug output

* Remove debug code

* Fixes for termination-log

* Allow init container to run as root

* Fixes for CentOS build

* Fixes for RHEL build

* Logging improvements

* Fix Dockerfile RHEL/CentOS build

* Fix bash error

* Make all builds specify UID

* Use redist client for Go SDK

* Inspect image before running tests

* New test for init container

* Log container runtime in runmqdevserver

* Add extra capabilities if using a RHEL image
This commit is contained in:
Arthur Barr
2019-02-25 15:44:14 +00:00
parent 2dbee560fe
commit cc0f072908
35 changed files with 871 additions and 504 deletions

View File

@@ -1,7 +1,7 @@
// +build mqdev
/*
© Copyright IBM Corporation 2018
© Copyright IBM Corporation 2018, 2019
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -114,7 +114,7 @@ func runJMSTests(t *testing.T, cli *client.Client, ID string, tls bool, user, pa
t.Fatal(err)
}
startContainer(t, cli, ctr.ID)
rc := waitForContainer(t, cli, ctr.ID, 10)
rc := waitForContainer(t, cli, ctr.ID, 2*time.Minute)
if rc != 0 {
t.Errorf("JUnit container failed with rc=%v", rc)
}

View File

@@ -47,11 +47,11 @@ func TestLicenseNotSet(t *testing.T) {
containerConfig := container.Config{}
id := runContainer(t, cli, &containerConfig)
defer cleanContainer(t, cli, id)
rc := waitForContainer(t, cli, id, 5)
rc := waitForContainer(t, cli, id, 10*time.Second)
if rc != 1 {
t.Errorf("Expected rc=1, got rc=%v", rc)
}
expectTerminationMessage(t)
expectTerminationMessage(t, cli, id)
}
func TestLicenseView(t *testing.T) {
@@ -65,7 +65,7 @@ func TestLicenseView(t *testing.T) {
}
id := runContainer(t, cli, &containerConfig)
defer cleanContainer(t, cli, id)
rc := waitForContainer(t, cli, id, 5)
rc := waitForContainer(t, cli, id, 10*time.Second)
if rc != 1 {
t.Errorf("Expected rc=1, got rc=%v", rc)
}
@@ -164,12 +164,12 @@ func TestSecurityVulnerabilitiesRedHat(t *testing.T) {
t.Fatal(err)
}
mnt = strings.TrimSpace(mnt)
_, _, err = command.Run("bash", "-c", "cp /etc/yum.repos.d/* "+ filepath.Join(mnt, "/etc/yum.repos.d/"))
_, _, err = command.Run("bash", "-c", "cp /etc/yum.repos.d/* "+filepath.Join(mnt, "/etc/yum.repos.d/"))
if err != nil {
t.Fatal(err)
}
out, ret, _ := command.Run("bash", "-c", "yum --installroot="+mnt+" updateinfo list sec | grep /Sec")
if ret != 1{
if ret != 1 {
t.Errorf("Expected no vulnerabilities, found the following:\n%v", out)
}
}
@@ -279,6 +279,70 @@ func TestNoVolumeWithRestart(t *testing.T) {
waitForReady(t, cli, id)
}
// TestVolumeRequiresRoot tests the case where only the root user can write
// to the persistent volume. In this case, an "init container" is needed,
// where `runmqserver -i` is run to initialize the storage. Then the
// container can be run as normal.
func TestVolumeRequiresRoot(t *testing.T) {
cli, err := client.NewEnvClient()
if err != nil {
t.Fatal(err)
}
vol := createVolume(t, cli)
defer removeVolume(t, cli, vol.Name)
// Set permissions on the volume to only allow root to write it
// It's important that read and execute permissions are given to other users
rc, _ := runContainerOneShotWithVolume(t, cli, vol.Name+":/mnt/mqm:nocopy", "bash", "-c", "chown 65534:4294967294 /mnt/mqm/ && chmod 0755 /mnt/mqm/ && ls -lan /mnt/mqm/")
if rc != 0 {
t.Errorf("Expected one shot container to return rc=0, got rc=%v", rc)
}
containerConfig := container.Config{
Image: imageName(),
Env: []string{"LICENSE=accept", "MQ_QMGR_NAME=qm1"},
}
hostConfig := container.HostConfig{
Binds: []string{
coverageBind(t),
vol.Name + ":/mnt/mqm:nocopy",
},
}
networkingConfig := network.NetworkingConfig{}
// Run an "init container" as root, with the "-i" option, to initialize the volume
containerConfig = container.Config{
Image: imageName(),
Env: []string{"LICENSE=accept", "MQ_QMGR_NAME=qm1", "DEBUG=true"},
User: "0",
Entrypoint: []string{"runmqserver", "-i"},
}
initCtr, err := cli.ContainerCreate(context.Background(), &containerConfig, &hostConfig, &networkingConfig, t.Name()+"Init")
if err != nil {
t.Fatal(err)
}
defer cleanContainer(t, cli, initCtr.ID)
t.Logf("Init container ID=%v", initCtr.ID)
startContainer(t, cli, initCtr.ID)
rc = waitForContainer(t, cli, initCtr.ID, 10*time.Second)
if rc != 0 {
t.Errorf("Expected init container to exit with rc=0, got rc=%v", rc)
}
containerConfig = container.Config{
Image: imageName(),
Env: []string{"LICENSE=accept", "MQ_QMGR_NAME=qm1", "DEBUG=true"},
}
ctr, err := cli.ContainerCreate(context.Background(), &containerConfig, &hostConfig, &networkingConfig, t.Name()+"Main")
if err != nil {
t.Fatal(err)
}
defer cleanContainer(t, cli, ctr.ID)
t.Logf("Main container ID=%v", ctr.ID)
startContainer(t, cli, ctr.ID)
waitForReady(t, cli, ctr.ID)
}
// TestCreateQueueManagerFail causes a failure of `crtmqm`
func TestCreateQueueManagerFail(t *testing.T) {
t.Parallel()
@@ -286,24 +350,31 @@ func TestCreateQueueManagerFail(t *testing.T) {
if err != nil {
t.Fatal(err)
}
img, _, err := cli.ImageInspectWithRaw(context.Background(), imageName())
if err != nil {
t.Fatal(err)
var files = []struct {
Name, Body string
}{
{"Dockerfile", fmt.Sprintf(`
FROM %v
USER root
RUN echo '#!/bin/bash\nexit 999' > /opt/mqm/bin/crtmqm
RUN chown mqm:mqm /opt/mqm/bin/crtmqm
RUN chmod 6550 /opt/mqm/bin/crtmqm
USER mqm`, imageName())},
}
oldEntrypoint := strings.Join(img.Config.Entrypoint, " ")
tag := createImage(t, cli, files)
defer deleteImage(t, cli, tag)
containerConfig := container.Config{
Env: []string{"LICENSE=accept", "MQ_QMGR_NAME=qm1"},
// Override the entrypoint to create the queue manager directory, but leave it empty.
// This will cause `crtmqm` to return with an exit code of 2.
Entrypoint: []string{"bash", "-c", "mkdir -p /mnt/mqm/data && mkdir -p /var/mqm/qmgrs/qm1 && exec " + oldEntrypoint},
Env: []string{"LICENSE=accept", "MQ_QMGR_NAME=qm1"},
Image: tag,
}
id := runContainer(t, cli, &containerConfig)
defer cleanContainer(t, cli, id)
rc := waitForContainer(t, cli, id, 10)
rc := waitForContainer(t, cli, id, 10*time.Second)
if rc != 1 {
t.Errorf("Expected rc=1, got rc=%v", rc)
}
expectTerminationMessage(t)
expectTerminationMessage(t, cli, id)
}
// TestStartQueueManagerFail causes a failure of `strmqm`
@@ -313,24 +384,31 @@ func TestStartQueueManagerFail(t *testing.T) {
if err != nil {
t.Fatal(err)
}
img, _, err := cli.ImageInspectWithRaw(context.Background(), imageName())
if err != nil {
t.Fatal(err)
var files = []struct {
Name, Body string
}{
{"Dockerfile", fmt.Sprintf(`
FROM %v
USER root
RUN echo '#!/bin/bash\ndltmqm $@ && strmqm $@' > /opt/mqm/bin/strmqm
RUN chown mqm:mqm /opt/mqm/bin/strmqm
RUN chmod 6550 /opt/mqm/bin/strmqm
USER mqm`, imageName())},
}
oldEntrypoint := strings.Join(img.Config.Entrypoint, " ")
tag := createImage(t, cli, files)
defer deleteImage(t, cli, tag)
containerConfig := container.Config{
Env: []string{"LICENSE=accept", "MQ_QMGR_NAME=qm1", "DEBUG=1"},
// Override the entrypoint to replace `strmqm` with a script which deletes the queue manager.
// This will cause `strmqm` to return with an exit code of 72.
Entrypoint: []string{"bash", "-c", "echo '#!/bin/bash\ndltmqm $@ && strmqm $@' > /opt/mqm/bin/strmqm && exec " + oldEntrypoint},
Env: []string{"LICENSE=accept", "MQ_QMGR_NAME=qm1"},
Image: tag,
}
id := runContainer(t, cli, &containerConfig)
defer cleanContainer(t, cli, id)
rc := waitForContainer(t, cli, id, 10)
rc := waitForContainer(t, cli, id, 10*time.Second)
if rc != 1 {
t.Errorf("Expected rc=1, got rc=%v", rc)
}
expectTerminationMessage(t)
expectTerminationMessage(t, cli, id)
}
// TestVolumeUnmount runs a queue manager with a volume, and then forces an
@@ -430,7 +508,13 @@ func TestMQSC(t *testing.T) {
var files = []struct {
Name, Body string
}{
{"Dockerfile", fmt.Sprintf("FROM %v\nRUN rm -f /etc/mqm/*.mqsc\nADD test.mqsc /etc/mqm/", imageName())},
{"Dockerfile", fmt.Sprintf(`
FROM %v
USER root
RUN rm -f /etc/mqm/*.mqsc
ADD test.mqsc /etc/mqm/
RUN chmod 0660 /etc/mqm/test.mqsc
USER mqm`, imageName())},
{"test.mqsc", "DEFINE QLOCAL(test)"},
}
tag := createImage(t, cli, files)
@@ -461,7 +545,13 @@ func TestInvalidMQSC(t *testing.T) {
var files = []struct {
Name, Body string
}{
{"Dockerfile", fmt.Sprintf("FROM %v\nRUN rm -f /etc/mqm/*.mqsc\nADD mqscTest.mqsc /etc/mqm/", imageName())},
{"Dockerfile", fmt.Sprintf(`
FROM %v
USER root
RUN rm -f /etc/mqm/*.mqsc
ADD mqscTest.mqsc /etc/mqm/
RUN chmod 0660 /etc/mqm/mqscTest.mqsc
USER mqm`, imageName())},
{"mqscTest.mqsc", "DEFINE INVALIDLISTENER('TEST.LISTENER.TCP') TRPTYPE(TCP) PORT(1414) CONTROL(QMGR) REPLACE"},
}
tag := createImage(t, cli, files)
@@ -473,11 +563,11 @@ func TestInvalidMQSC(t *testing.T) {
}
id := runContainer(t, cli, &containerConfig)
defer cleanContainer(t, cli, id)
rc := waitForContainer(t, cli, id, 5)
rc := waitForContainer(t, cli, id, 60*time.Second)
if rc != 1 {
t.Errorf("Expected rc=1, got rc=%v", rc)
}
expectTerminationMessage(t)
expectTerminationMessage(t, cli, id)
}
// TestReadiness creates a new image with large amounts of MQSC in, to
@@ -497,7 +587,13 @@ func TestReadiness(t *testing.T) {
var files = []struct {
Name, Body string
}{
{"Dockerfile", fmt.Sprintf("FROM %v\nRUN rm -f /etc/mqm/*.mqsc\nADD test.mqsc /etc/mqm/", imageName())},
{"Dockerfile", fmt.Sprintf(`
FROM %v
USER root
RUN rm -f /etc/mqm/*.mqsc
ADD test.mqsc /etc/mqm/
RUN chmod 0660 /etc/mqm/test.mqsc
USER mqm`, imageName())},
{"test.mqsc", buf.String()},
}
tag := createImage(t, cli, files)
@@ -656,11 +752,11 @@ func TestBadLogFormat(t *testing.T) {
}
id := runContainer(t, cli, &containerConfig)
defer cleanContainer(t, cli, id)
rc := waitForContainer(t, cli, id, 5)
rc := waitForContainer(t, cli, id, 5*time.Second)
if rc != 1 {
t.Errorf("Expected rc=1, got rc=%v", rc)
}
expectTerminationMessage(t)
expectTerminationMessage(t, cli, id)
}
// TestMQJSONDisabled tests the case where MQ's JSON logging feature is

View File

@@ -1,5 +1,5 @@
/*
© Copyright IBM Corporation 2017, 2018
© Copyright IBM Corporation 2017, 2019
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -59,6 +59,18 @@ func imageNameDevJMS() string {
return image
}
func baseImage(t *testing.T, cli *client.Client) string {
rc, out := runContainerOneShot(t, cli, "bash", "-c", "cat /etc/*release | grep \"^ID=\"")
if rc != 0 {
t.Fatal("Couldn't determine base image")
}
s := strings.Split(out, "=")
if len(s) < 2 {
t.Fatal("Couldn't determine base image string")
}
return s[1]
}
// isWSL return whether we are running in the Windows Subsystem for Linux
func isWSL(t *testing.T) bool {
if runtime.GOOS == "linux" {
@@ -124,46 +136,31 @@ func getTempDir(t *testing.T, unixStylePath bool) string {
return "/tmp/"
}
// terminationLogUnixPath returns the name of the file to use for the termination log message, with a UNIX path
func terminationLogUnixPath(t *testing.T) string {
// Warning: this directory must be accessible to the Docker daemon,
// in order to enable the bind mount
return getTempDir(t, true) + t.Name() + "-termination-log"
}
// terminationLogOSPath returns the name of the file to use for the termination log message, with an OS specific path
func terminationLogOSPath(t *testing.T) string {
// Warning: this directory must be accessible to the Docker daemon,
// in order to enable the bind mount
return getTempDir(t, false) + t.Name() + "-termination-log"
}
// terminationBind returns a string to use to bind-mount a termination log file.
// This is done using a bind, because you can't copy files from /dev out of the container.
func terminationBind(t *testing.T) string {
n := terminationLogUnixPath(t)
// Remove it if it already exists
os.Remove(n)
// Create the empty file
f, err := os.OpenFile(n, os.O_WRONLY|os.O_CREATE, 0600)
if err != nil {
t.Fatal(err)
}
f.Close()
return terminationLogOSPath(t) + ":/dev/termination-log"
}
// terminationMessage return the termination message, or an empty string if not set
func terminationMessage(t *testing.T) string {
b, err := ioutil.ReadFile(terminationLogUnixPath(t))
func terminationMessage(t *testing.T, cli *client.Client, ID string) string {
r, _, err := cli.CopyFromContainer(context.Background(), ID, "/run/termination-log")
if err != nil {
t.Log(err)
return ""
}
return string(b)
b, err := ioutil.ReadAll(r)
tr := tar.NewReader(bytes.NewReader(b))
_, err = tr.Next()
if err != nil {
t.Log(err)
return ""
}
// read the complete content of the file h.Name into the bs []byte
content, err := ioutil.ReadAll(tr)
if err != nil {
t.Log(err)
return ""
}
return string(content)
}
func expectTerminationMessage(t *testing.T) {
m := terminationMessage(t)
func expectTerminationMessage(t *testing.T, cli *client.Client, ID string) {
m := terminationMessage(t, cli, ID)
if m == "" {
t.Error("Expected termination message to be set")
}
@@ -195,11 +192,10 @@ func cleanContainer(t *testing.T, cli *client.Client, ID string) {
// Log the container output for any container we're about to delete
t.Logf("Console log from container %v:\n%v", ID, inspectTextLogs(t, cli, ID))
m := terminationMessage(t)
m := terminationMessage(t, cli, ID)
if m != "" {
t.Logf("Termination message: %v", m)
}
os.Remove(terminationLogUnixPath(t))
t.Logf("Removing container: %s", ID)
opts := types.ContainerRemoveOptions{
@@ -212,6 +208,22 @@ func cleanContainer(t *testing.T, cli *client.Client, ID string) {
}
}
// devImage returns true if the specified image is a developer image,
// determined by use of the MQ_ADMIN_PASSWORD or MQ_APP_PASSWORD
// environment variables
func devImage(t *testing.T, cli *client.Client, imageID string) bool {
i, _, err := cli.ImageInspectWithRaw(context.Background(), imageID)
if err != nil {
t.Fatal(err)
}
for _, e := range i.ContainerConfig.Env {
if strings.HasPrefix(e, "MQ_ADMIN_PASSWORD") || strings.HasPrefix(e, "MQ_APP_PASSWORD") {
return true
}
}
return false
}
// runContainerWithPorts creates and starts a container, exposing the specified ports on the host.
// If no image is specified in the container config, then the image name is retrieved from the TEST_IMAGE
// environment variable.
@@ -219,15 +231,35 @@ func runContainerWithPorts(t *testing.T, cli *client.Client, containerConfig *co
if containerConfig.Image == "" {
containerConfig.Image = imageName()
}
// Always run as the "mqm" user, unless the test has specified otherwise
if containerConfig.User == "" {
containerConfig.User = "mqm"
}
// if coverage
containerConfig.Env = append(containerConfig.Env, "COVERAGE_FILE="+t.Name()+".cov")
containerConfig.Env = append(containerConfig.Env, "EXIT_CODE_FILE="+getExitCodeFilename(t))
hostConfig := container.HostConfig{
Binds: []string{
coverageBind(t),
terminationBind(t),
// terminationBind(t),
},
PortBindings: nat.PortMap{},
CapDrop: []string{
"ALL",
},
}
if devImage(t, cli, containerConfig.Image) {
t.Logf("Detected MQ Advanced for Developers image — adding extra Linux capabilities to container")
hostConfig.CapAdd = []string{
"CHOWN",
"SETUID",
"SETGID",
"AUDIT_WRITE",
}
// Only needed for a RHEL-based image
if baseImage(t, cli) != "ubuntu" {
hostConfig.CapAdd = append(hostConfig.CapAdd, "DAC_OVERRIDE")
}
}
for _, p := range ports {
port := nat.Port(fmt.Sprintf("%v/tcp", p))
@@ -254,13 +286,49 @@ func runContainer(t *testing.T, cli *client.Client, containerConfig *container.C
return runContainerWithPorts(t, cli, containerConfig, nil)
}
// runContainerOneShot runs a container with a custom entrypoint, as the root
// user and with default capabilities
func runContainerOneShot(t *testing.T, cli *client.Client, command ...string) (int64, string) {
containerConfig := container.Config{
Entrypoint: command,
User: "root",
Image: imageName(),
}
id := runContainer(t, cli, &containerConfig)
defer cleanContainer(t, cli, id)
return waitForContainer(t, cli, id, 10), inspectLogs(t, cli, id)
hostConfig := container.HostConfig{}
networkingConfig := network.NetworkingConfig{}
t.Logf("Running one shot container (%s)", containerConfig.Image)
ctr, err := cli.ContainerCreate(context.Background(), &containerConfig, &hostConfig, &networkingConfig, t.Name())
if err != nil {
t.Fatal(err)
}
startContainer(t, cli, ctr.ID)
defer cleanContainer(t, cli, ctr.ID)
return waitForContainer(t, cli, ctr.ID, 10*time.Second), inspectLogs(t, cli, ctr.ID)
}
// runContainerOneShot runs a container with a custom entrypoint, as the root
// user, with default capabilities, and a volume mounted
func runContainerOneShotWithVolume(t *testing.T, cli *client.Client, bind string, command ...string) (int64, string) {
containerConfig := container.Config{
Entrypoint: command,
User: "root",
Image: imageName(),
}
hostConfig := container.HostConfig{
Binds: []string{
bind,
},
}
networkingConfig := network.NetworkingConfig{}
t.Logf("Running one shot container with volume (%s): %v", containerConfig.Image, command)
ctr, err := cli.ContainerCreate(context.Background(), &containerConfig, &hostConfig, &networkingConfig, t.Name())
if err != nil {
t.Fatal(err)
}
t.Logf("One shot container ID: %v", ctr.ID)
startContainer(t, cli, ctr.ID)
defer cleanContainer(t, cli, ctr.ID)
return waitForContainer(t, cli, ctr.ID, 10*time.Second), inspectLogs(t, cli, ctr.ID)
}
func startContainer(t *testing.T, cli *client.Client, ID string) {
@@ -309,19 +377,19 @@ func getCoverageExitCode(t *testing.T, orig int64) int64 {
}
// waitForContainer waits until a container has exited
func waitForContainer(t *testing.T, cli *client.Client, ID string, timeout int64) int64 {
rc, err := cli.ContainerWait(context.Background(), ID)
func waitForContainer(t *testing.T, cli *client.Client, ID string, timeout time.Duration) int64 {
c, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
rc, err := cli.ContainerWait(c, ID)
if err != nil {
t.Fatal(err)
}
if coverage() {
// COVERAGE: When running coverage, the exit code is written to a file,
// to allow the coverage to be generated (which doesn't happen for non-zero
// exit codes)
rc = getCoverageExitCode(t, rc)
}
if err != nil {
t.Fatal(err)
}
return rc
}
@@ -395,7 +463,7 @@ func execContainer(t *testing.T, cli *client.Client, ID string, user string, cmd
}
func waitForReady(t *testing.T, cli *client.Client, ID string) {
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
for {