Pass secrets more securely for container action

This commit is contained in:
Thomas Boop
2022-06-06 18:43:57 -04:00
parent 689a74e352
commit e928fa3252
6 changed files with 84 additions and 27 deletions

View File

@@ -1,5 +1,6 @@
import { podPrune } from '../k8s' import { pruneSecrets, prunePods } from '../k8s'
export async function cleanupJob(): Promise<void> { export async function cleanupJob(): Promise<void> {
await podPrune() await prunePods()
await pruneSecrets()
} }

View File

@@ -20,7 +20,7 @@ export function getJobPodName(): string {
export function getStepPodName(): string { export function getStepPodName(): string {
return `${getRunnerPodName().substring( return `${getRunnerPodName().substring(
0, 0,
MAX_POD_NAME_LENGTH - ('-step'.length + STEP_POD_NAME_SUFFIX_LENGTH) MAX_POD_NAME_LENGTH - ('-step-'.length + STEP_POD_NAME_SUFFIX_LENGTH)
)}-step-${uuidv4().substring(0, STEP_POD_NAME_SUFFIX_LENGTH)}` )}-step-${uuidv4().substring(0, STEP_POD_NAME_SUFFIX_LENGTH)}`
} }
@@ -34,6 +34,13 @@ export function getVolumeClaimName(): string {
return name return name
} }
export function getSecretName(): string {
return `${getRunnerPodName().substring(
0,
MAX_POD_NAME_LENGTH - ('-secret-'.length + STEP_POD_NAME_SUFFIX_LENGTH)
)}-secret-${uuidv4().substring(0, STEP_POD_NAME_SUFFIX_LENGTH)}`
}
const MAX_POD_NAME_LENGTH = 63 const MAX_POD_NAME_LENGTH = 63
const STEP_POD_NAME_SUFFIX_LENGTH = 8 const STEP_POD_NAME_SUFFIX_LENGTH = 8
export const JOB_CONTAINER_NAME = 'job' export const JOB_CONTAINER_NAME = 'job'

View File

@@ -14,7 +14,7 @@ import {
isAuthPermissionsOK, isAuthPermissionsOK,
isPodContainerAlpine, isPodContainerAlpine,
namespace, namespace,
podPrune, prunePods,
requiredPermissions, requiredPermissions,
waitForPodPhases waitForPodPhases
} from '../k8s' } from '../k8s'
@@ -29,7 +29,7 @@ export async function prepareJob(
args: prepareJobArgs, args: prepareJobArgs,
responseFile responseFile
): Promise<void> { ): Promise<void> {
await podPrune() await prunePods()
if (!(await isAuthPermissionsOK())) { if (!(await isAuthPermissionsOK())) {
throw new Error( throw new Error(
`The Service account needs the following permissions ${JSON.stringify( `The Service account needs the following permissions ${JSON.stringify(
@@ -58,7 +58,7 @@ export async function prepareJob(
try { try {
createdPod = await createPod(container, services, args.registry) createdPod = await createPod(container, services, args.registry)
} catch (err) { } catch (err) {
await podPrune() await prunePods()
throw new Error(`failed to create job pod: ${JSON.stringify(err)}`) throw new Error(`failed to create job pod: ${JSON.stringify(err)}`)
} }
@@ -73,7 +73,7 @@ export async function prepareJob(
new Set([PodPhase.PENDING]) new Set([PodPhase.PENDING])
) )
} catch (err) { } catch (err) {
await podPrune() await prunePods()
throw new Error(`Pod failed to come online with error: ${err}`) throw new Error(`Pod failed to come online with error: ${err}`)
} }

View File

@@ -3,6 +3,7 @@ import * as core from '@actions/core'
import { PodPhase } from 'hooklib' import { PodPhase } from 'hooklib'
import { import {
createJob, createJob,
createSecretForEnvs,
getContainerJobPodName, getContainerJobPodName,
getPodLogs, getPodLogs,
getPodStatus, getPodStatus,
@@ -16,7 +17,13 @@ export async function runContainerStep(stepContainer): Promise<number> {
if (stepContainer.dockerfile) { if (stepContainer.dockerfile) {
throw new Error('Building container actions is not currently supported') throw new Error('Building container actions is not currently supported')
} }
const container = createPodSpec(stepContainer) let secretName: string | undefined = undefined
if (stepContainer['environmentVariables']) {
secretName = await createSecretForEnvs(
stepContainer['environmentVariables']
)
}
const container = createPodSpec(stepContainer, secretName)
const job = await createJob(container) const job = await createJob(container)
if (!job.metadata?.name) { if (!job.metadata?.name) {
throw new Error( throw new Error(
@@ -39,28 +46,28 @@ export async function runContainerStep(stepContainer): Promise<number> {
core.warning(`Can't determine container status`) core.warning(`Can't determine container status`)
return 0 return 0
} }
const exitCode = const exitCode =
status.containerStatuses[status.containerStatuses.length - 1].state status.containerStatuses[status.containerStatuses.length - 1].state
?.terminated?.exitCode ?.terminated?.exitCode
return Number(exitCode) || 0 return Number(exitCode) || 0
} }
function createPodSpec(container): k8s.V1Container { function createPodSpec(container, secretName?: string): k8s.V1Container {
const podContainer = new k8s.V1Container() const podContainer = new k8s.V1Container()
podContainer.name = JOB_CONTAINER_NAME podContainer.name = JOB_CONTAINER_NAME
podContainer.image = container.image podContainer.image = container.image
if (container.entryPoint) { if (container.entryPoint) {
podContainer.command = [container.entryPoint, ...container.entryPointArgs] podContainer.command = [container.entryPoint, ...container.entryPointArgs]
} }
if (secretName) {
podContainer.env = [] podContainer.envFrom = [
for (const [key, value] of Object.entries( {
container['environmentVariables'] secretRef: {
)) { name: secretName,
if (value && key !== 'HOME') { optional: false
podContainer.env.push({ name: key, value: value as string }) }
} }
]
} }
podContainer.volumeMounts = containerVolumes() podContainer.volumeMounts = containerVolumes()

View File

@@ -1,10 +1,10 @@
import * as k8s from '@kubernetes/client-node' import * as k8s from '@kubernetes/client-node'
import { ContainerInfo, PodPhase, Registry } from 'hooklib' import { ContainerInfo, PodPhase, Registry } from 'hooklib'
import * as stream from 'stream' import * as stream from 'stream'
import { v4 as uuidv4 } from 'uuid'
import { import {
getJobPodName, getJobPodName,
getRunnerPodName, getRunnerPodName,
getSecretName,
getStepPodName, getStepPodName,
getVolumeClaimName, getVolumeClaimName,
RunnerInstanceLabel RunnerInstanceLabel
@@ -251,12 +251,13 @@ export async function createDockerSecret(
} }
} }
} }
const secretName = generateSecretName() const secretName = getSecretName()
const secret = new k8s.V1Secret() const secret = new k8s.V1Secret()
secret.immutable = true secret.immutable = true
secret.apiVersion = 'v1' secret.apiVersion = 'v1'
secret.metadata = new k8s.V1ObjectMeta() secret.metadata = new k8s.V1ObjectMeta()
secret.metadata.name = secretName secret.metadata.name = secretName
secret.metadata.labels = { 'runner-pod': getRunnerPodName() }
secret.kind = 'Secret' secret.kind = 'Secret'
secret.data = { secret.data = {
'.dockerconfigjson': Buffer.from( '.dockerconfigjson': Buffer.from(
@@ -269,6 +270,53 @@ export async function createDockerSecret(
return body return body
} }
export async function createSecretForEnvs(envs: {
[key: string]: string
}): Promise<string> {
const secret = new k8s.V1Secret()
const secretName = getSecretName()
secret.immutable = true
secret.apiVersion = 'v1'
secret.metadata = new k8s.V1ObjectMeta()
secret.metadata.name = secretName
secret.metadata.labels = { 'runner-pod': getRunnerPodName() }
secret.kind = 'Secret'
secret.data = {}
for (const [key, value] of Object.entries(envs)) {
secret.data[key] = Buffer.from(value).toString('base64')
}
try {
await k8sApi.createNamespacedSecret(namespace(), secret)
} catch (e) {
throw e
}
return secretName
}
export async function deleteSecret(secretName: string): Promise<void> {
await k8sApi.deleteNamespacedSecret(secretName, namespace())
}
export async function pruneSecrets(): Promise<void> {
const secretList = await k8sApi.listNamespacedSecret(
namespace(),
undefined,
undefined,
undefined,
undefined,
new RunnerInstanceLabel().toString()
)
if (!secretList.body.items.length) {
return
}
await Promise.all(
secretList.body.items.map(
secret => secret.metadata?.name && deleteSecret(secret.metadata.name)
)
)
}
export async function waitForPodPhases( export async function waitForPodPhases(
podName: string, podName: string,
awaitingPhases: Set<PodPhase>, awaitingPhases: Set<PodPhase>,
@@ -346,7 +394,7 @@ export async function getPodLogs(
await new Promise(resolve => r.on('close', () => resolve(null))) await new Promise(resolve => r.on('close', () => resolve(null)))
} }
export async function podPrune(): Promise<void> { export async function prunePods(): Promise<void> {
const podList = await k8sApi.listNamespacedPod( const podList = await k8sApi.listNamespacedPod(
namespace(), namespace(),
undefined, undefined,
@@ -460,10 +508,6 @@ export function namespace(): string {
return context.namespace return context.namespace
} }
function generateSecretName(): string {
return `github-secret-${uuidv4()}`
}
function runnerName(): string { function runnerName(): string {
const name = process.env.ACTIONS_RUNNER_POD_NAME const name = process.env.ACTIONS_RUNNER_POD_NAME
if (!name) { if (!name) {

View File

@@ -27,7 +27,5 @@ describe('Run container step', () => {
}) })
afterEach(async () => { afterEach(async () => {
await testHelper.cleanup() await testHelper.cleanup()
// wait for the job cleanup
await new Promise(resolve => setTimeout(resolve, 300 * 1000))
}) })
}) })