Skip to content

Commit

Permalink
feat(container): more security options
Browse files Browse the repository at this point in the history
We now make the `privileged` and `capabilities` security context
fields available in service, test and task configs for `container`
modules.

This adds a degree of flexibility for many use cases.
  • Loading branch information
thsig committed Sep 13, 2021
1 parent e453d70 commit c0f14e1
Show file tree
Hide file tree
Showing 10 changed files with 445 additions and 7 deletions.
40 changes: 40 additions & 0 deletions core/src/plugins/container/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,9 @@ export interface ContainerServiceSpec extends CommonServiceSpec {
ports: ServicePortSpec[]
replicas?: number
volumes: ContainerVolumeSpec[]
privileged?: boolean
addCapabilities?: string[]
dropCapabilities?: string[]
}

export const commandExample = ["/bin/sh", "-c"]
Expand Down Expand Up @@ -504,6 +507,28 @@ export function getContainerVolumesSchema(targetType: string) {
`)
}

const containerPrivilegedSchema = (targetType: string) =>
joi
.boolean()
.optional()
.description(
`If true, run the ${targetType}'s main container in privileged mode. Processes in privileged containers are essentially equivalent to root on the host. Defaults to false.`
)

const containerAddCapabilitiesSchema = (targetType: string) =>
joi
.array()
.items(joi.string())
.optional()
.description(`POSIX capabilities to add to the running ${targetType}'s main container.`)

const containerDropCapabilitiesSchema = (targetType: string) =>
joi
.array()
.items(joi.string())
.optional()
.description(`POSIX capabilities to remove from the running ${targetType}'s main container.`)

const containerServiceSchema = () =>
baseServiceSpecSchema().keys({
annotations: annotationsSchema().description(
Expand Down Expand Up @@ -565,6 +590,9 @@ const containerServiceSchema = () =>
with hot-reloading enabled, or if the provider doesn't support multiple replicas.
`),
volumes: getContainerVolumesSchema("service"),
privileged: containerPrivilegedSchema("service"),
addCapabilities: containerAddCapabilitiesSchema("service"),
dropCapabilities: containerDropCapabilitiesSchema("service"),
})

export interface ContainerRegistryConfig {
Expand Down Expand Up @@ -643,6 +671,9 @@ export interface ContainerTestSpec extends BaseTestSpec {
cpu: ContainerResourcesSpec["cpu"]
memory: ContainerResourcesSpec["memory"]
volumes: ContainerVolumeSpec[]
privileged?: boolean
addCapabilities?: string[]
dropCapabilities?: string[]
}

export const containerTestSchema = () =>
Expand All @@ -662,6 +693,9 @@ export const containerTestSchema = () =>
cpu: containerCpuSchema("test").default(defaultContainerResources.cpu),
memory: containerMemorySchema("test").default(defaultContainerResources.memory),
volumes: getContainerVolumesSchema("test"),
privileged: containerPrivilegedSchema("test"),
addCapabilities: containerAddCapabilitiesSchema("test"),
dropCapabilities: containerDropCapabilitiesSchema("test"),
})

export interface ContainerTaskSpec extends BaseTaskSpec {
Expand All @@ -673,6 +707,9 @@ export interface ContainerTaskSpec extends BaseTaskSpec {
cpu: ContainerResourcesSpec["cpu"]
memory: ContainerResourcesSpec["memory"]
volumes: ContainerVolumeSpec[]
privileged?: boolean
addCapabilities?: string[]
dropCapabilities?: string[]
}

export const containerTaskSchema = () =>
Expand All @@ -694,6 +731,9 @@ export const containerTaskSchema = () =>
cpu: containerCpuSchema("task").default(defaultContainerResources.cpu),
memory: containerMemorySchema("task").default(defaultContainerResources.memory),
volumes: getContainerVolumesSchema("task"),
privileged: containerPrivilegedSchema("task"),
addCapabilities: containerAddCapabilitiesSchema("task"),
dropCapabilities: containerDropCapabilitiesSchema("task"),
})
.description("A task that can be run in the container.")

Expand Down
3 changes: 2 additions & 1 deletion core/src/plugins/kubernetes/container/deployment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import { prepareImagePullSecrets } from "../secrets"
import { configureHotReload } from "../hot-reload/helpers"
import { configureDevMode, startDevModeSync } from "../dev-mode"
import { hotReloadableKinds, HotReloadableResource } from "../hot-reload/hot-reload"
import { getResourceRequirements } from "./util"
import { getResourceRequirements, getSecurityContext } from "./util"

export const DEFAULT_CPU_REQUEST = "10m"
export const DEFAULT_MEMORY_REQUEST = "90Mi" // This is the minimum in some clusters
Expand Down Expand Up @@ -402,6 +402,7 @@ export async function createWorkloadManifest({
imagePullPolicy: "IfNotPresent",
securityContext: {
allowPrivilegeEscalation: false,
...getSecurityContext(spec.privileged, spec.addCapabilities, spec.dropCapabilities),
},
}

Expand Down
22 changes: 20 additions & 2 deletions core/src/plugins/kubernetes/container/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export async function runContainerModule(params: RunModuleParams<ContainerModule

export async function runContainerService(params: RunServiceParams<ContainerModule>): Promise<RunResult> {
const { module, ctx, log, service, runtimeContext, interactive, timeout } = params
const { command, args, env } = service.spec
const { command, args, env, privileged, addCapabilities, dropCapabilities } = service.spec

runtimeContext.envVars = { ...runtimeContext.envVars, ...env }

Expand All @@ -58,6 +58,9 @@ export async function runContainerService(params: RunServiceParams<ContainerModu
runtimeContext,
namespace: namespaceStatus.namespaceName,
version: service.version,
privileged,
addCapabilities,
dropCapabilities,
})

return {
Expand All @@ -68,7 +71,19 @@ export async function runContainerService(params: RunServiceParams<ContainerModu

export async function runContainerTask(params: RunTaskParams<ContainerModule>): Promise<RunTaskResult> {
const { ctx, log, module, task } = params
const { args, command, artifacts, env, cpu, memory, timeout, volumes } = task.spec
const {
args,
command,
artifacts,
env,
cpu,
memory,
timeout,
volumes,
privileged,
addCapabilities,
dropCapabilities,
} = task.spec

const image = module.outputs["deployment-image-id"]
const k8sCtx = ctx as KubernetesPluginContext
Expand All @@ -88,6 +103,9 @@ export async function runContainerTask(params: RunTaskParams<ContainerModule>):
timeout: timeout || undefined,
volumes,
version: task.version,
privileged,
addCapabilities,
dropCapabilities,
})

const result: RunTaskResult = {
Expand Down
16 changes: 15 additions & 1 deletion core/src/plugins/kubernetes/container/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,18 @@ import { KubernetesPluginContext } from "../config"

export async function testContainerModule(params: TestModuleParams<ContainerModule>): Promise<TestResult> {
const { ctx, module, test, log } = params
const { command, args, artifacts, env, cpu, memory, volumes } = test.config.spec
const {
command,
args,
artifacts,
env,
cpu,
memory,
volumes,
privileged,
addCapabilities,
dropCapabilities,
} = test.config.spec
const testName = test.name
const timeout = test.config.timeout || DEFAULT_TEST_TIMEOUT
const k8sCtx = ctx as KubernetesPluginContext
Expand All @@ -40,6 +51,9 @@ export async function testContainerModule(params: TestModuleParams<ContainerModu
timeout,
version: test.version,
volumes,
privileged,
addCapabilities,
dropCapabilities,
})

return storeTestResult({
Expand Down
23 changes: 22 additions & 1 deletion core/src/plugins/kubernetes/container/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { KubernetesPluginContext } from "../config"
import { getSystemNamespace } from "../namespace"
import { got, GotTextOptions } from "../../../util/http"
import { ContainerResourcesSpec, ServiceLimitSpec } from "../../container/config"
import { V1ResourceRequirements } from "@kubernetes/client-node"
import { V1ResourceRequirements, V1SecurityContext } from "@kubernetes/client-node"
import { kilobytesToString, millicpuToString } from "../util"

export async function queryRegistry(ctx: KubernetesPluginContext, log: LogEntry, path: string, opts?: GotTextOptions) {
Expand Down Expand Up @@ -54,3 +54,24 @@ export function getResourceRequirements(
},
}
}

export function getSecurityContext(
privileged: boolean | undefined,
addCapabilities: string[] | undefined,
dropCapabilities: string[] | undefined
): V1SecurityContext | null {
if (!privileged && !addCapabilities && !dropCapabilities) {
return null
}
const ctx: V1SecurityContext = {}
if (privileged) {
ctx.privileged = privileged
}
if (addCapabilities) {
ctx.capabilities = { add: addCapabilities }
}
if (dropCapabilities) {
ctx.capabilities = { ...(ctx.capabilities || {}), drop: dropCapabilities }
}
return ctx
}
19 changes: 18 additions & 1 deletion core/src/plugins/kubernetes/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import { configureVolumes } from "./container/deployment"
import { PluginContext } from "../../plugin-context"
import { waitForResources, ResourceStatus } from "./status/status"
import { RuntimeContext } from "../../runtime-context"
import { getResourceRequirements } from "./container/util"
import { getResourceRequirements, getSecurityContext } from "./container/util"
import { KUBECTL_DEFAULT_TIMEOUT } from "./kubectl"
import { copy } from "fs-extra"

Expand Down Expand Up @@ -114,6 +114,9 @@ export async function runAndCopy({
namespace,
version,
volumes,
privileged,
addCapabilities,
dropCapabilities,
}: RunModuleParams<GardenModule> & {
image: string
container?: V1Container
Expand All @@ -127,6 +130,9 @@ export async function runAndCopy({
namespace: string
version: string
volumes?: ContainerVolumeSpec[]
privileged?: boolean
addCapabilities?: string[]
dropCapabilities?: string[]
}): Promise<RunResult> {
const provider = <KubernetesProvider>ctx.provider
const api = await KubeApi.factory(log, ctx, provider)
Expand Down Expand Up @@ -159,6 +165,9 @@ export async function runAndCopy({
container,
namespace,
volumes,
privileged,
addCapabilities,
dropCapabilities,
})

if (!podName) {
Expand Down Expand Up @@ -225,6 +234,9 @@ export async function prepareRunPodSpec({
container,
namespace,
volumes,
privileged,
addCapabilities,
dropCapabilities,
}: {
podSpec?: V1PodSpec
getArtifacts: boolean
Expand All @@ -244,6 +256,9 @@ export async function prepareRunPodSpec({
container?: V1Container
namespace: string
volumes?: ContainerVolumeSpec[]
privileged?: boolean
addCapabilities?: string[]
dropCapabilities?: string[]
}): Promise<V1PodSpec> {
// Prepare environment variables
envVars = { ...runtimeContext.envVars, ...envVars }
Expand All @@ -254,11 +269,13 @@ export async function prepareRunPodSpec({
])

const resourceRequirements = resources ? { resources: getResourceRequirements(resources) } : {}
const securityContext = getSecurityContext(privileged, addCapabilities, dropCapabilities)

const containers: V1Container[] = [
{
...omit(container || {}, runContainerExcludeFields),
...resourceRequirements,
...(securityContext ? { securityContext } : {}),
// We always override the following attributes
name: mainContainerName,
image,
Expand Down
32 changes: 31 additions & 1 deletion core/test/integ/src/plugins/kubernetes/container/deployment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import { pathExists, readFile, remove, writeFile } from "fs-extra"
import { execInWorkload, kilobytesToString, millicpuToString } from "../../../../../../src/plugins/kubernetes/util"
import { getResourceRequirements } from "../../../../../../src/plugins/kubernetes/container/util"

describe("kubernetes container deployment handlers", () => {
describe.only("kubernetes container deployment handlers", () => {
let garden: Garden
let graph: ConfigGraph
let ctx: KubernetesPluginContext
Expand Down Expand Up @@ -180,6 +180,36 @@ describe("kubernetes container deployment handlers", () => {
})
})

it("should apply security context fields if specified", async () => {
const service = graph.getService("simple-service")
const namespace = provider.config.namespace!.name!
service.spec.privileged = true
service.spec.addCapabilities = ["SYS_TIME"]
service.spec.dropCapabilities = ["NET_ADMIN"]

const resource = await createWorkloadManifest({
api,
provider,
service,
runtimeContext: emptyRuntimeContext,
namespace,
enableDevMode: false,
enableHotReload: false,
log: garden.log,
production: false,
blueGreen: false,
})

expect(resource.spec.template?.spec?.containers[0].securityContext).to.eql({
allowPrivilegeEscalation: false,
privileged: true,
capabilities: {
add: ["SYS_TIME"],
drop: ["NET_ADMIN"],
},
})
})

it("should increase liveness probes when in hot-reload mode", async () => {
const service = graph.getService("hot-reload")
const namespace = provider.config.namespace!.name!
Expand Down
Loading

0 comments on commit c0f14e1

Please sign in to comment.