Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added support for strongbox decryption #118

Merged
merged 4 commits into from
Sep 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@ FROM golang:1.21-alpine as builder
ARG TARGETOS
ARG TARGETARCH

ENV STRONGBOX_VERSION=1.1.0

RUN os=$(go env GOOS) && arch=$(go env GOARCH) \
&& apk --no-cache add curl git \
&& curl -Ls https://github.com/uw-labs/strongbox/releases/download/v${STRONGBOX_VERSION}/strongbox_${STRONGBOX_VERSION}_${os}_${arch} \
> /usr/local/bin/strongbox \
&& chmod +x /usr/local/bin/strongbox

WORKDIR /workspace
# Copy the Go Modules manifests
COPY go.mod go.mod
Expand Down Expand Up @@ -30,6 +38,8 @@ ENV USER_ID=65532
RUN adduser -S -H -u $USER_ID tf-applier \
&& apk --no-cache add ca-certificates git openssh-client

COPY --from=builder /usr/local/bin/strongbox /usr/local/bin/

WORKDIR /
COPY --from=builder /workspace/tf-applier .

Expand Down
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ spec:
secretKeyRef:
name: hello-module-secrets
key: AWS_SECRET_KEY
- name: TF_APPLIER_STRONGBOX_KEYRING
valueFrom:
secretKeyRef:
name: hello-module-secrets
key: strongbox_keyring
var:
- name: image_id
value: ami-abc123
Expand Down Expand Up @@ -94,6 +99,12 @@ module "storage" {
```
Since key is set on controller it can be used by ALL modules managed by the controller. Terraform applier doesn't support private key per module yet.

### Strongbox decryption

Terraform applier supports strongbox decryption, its triggered if `TF_APPLIER_STRONGBOX_KEYRING` EVN is set on module.
content of this ENV should be valid strongbox keyring file data which should include strongbox key used to encrypt secrets in the module.
TF Applier will also configure Git and Strongbox Home before running `init` to decrypt any encrypted file from remote base as well.

### Graceful shutdown

To make sure all terraform module run does complete in finite time `runTimeout` is added to the module spec.
Expand Down
4 changes: 4 additions & 0 deletions integration_test/.tests_strongbox_keyring
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
keyentries:
- description: tf-applier-test
key-id: JXm+9DGOuYW8d9/DE6VIuowv73JH7cn0vDDFkH/J1LA=
key: fDT05Ggm6aStwgn6fo7yfQ+YB7RYY6B5FjakLGicRvE=
21 changes: 21 additions & 0 deletions integration_test/module_controller_with_runner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package integration_test

import (
"context"
"fmt"
"os"
"path/filepath"
"time"
Expand Down Expand Up @@ -32,6 +33,12 @@ var _ = Describe("Module controller with Runner", func() {
boolTrue = true
)

sbKeyringData, err := os.ReadFile(".tests_strongbox_keyring")
if err != nil {
fmt.Println(err)
Panic()
}

Context("When creating Module", func() {
BeforeEach(func() {
// reset Time
Expand Down Expand Up @@ -85,6 +92,9 @@ var _ = Describe("Module controller with Runner", func() {
Schedule: "50 * * * *",
RepoName: repo,
Path: path,
Env: []corev1.EnvVar{
{Name: "TF_APPLIER_STRONGBOX_KEYRING", Value: string(sbKeyringData)},
},
},
}
Expect(k8sClient.Create(ctx, module)).Should(Succeed())
Expand Down Expand Up @@ -136,6 +146,9 @@ var _ = Describe("Module controller with Runner", func() {
Expect(fetchedModule.Status.LastApplyInfo.Output).Should(ContainSubstring("Apply complete!"))
Expect(fetchedModule.Status.LastApplyInfo.Timestamp.UTC()).Should(Equal(fakeClock.T.UTC()))

// make sure secret values are there in output (strongbox decryption was successful)
Expect(fetchedModule.Status.LastApplyInfo.Output).Should(ContainSubstring("TOP_SECRET_VALUE"))

// delete module to stopping requeue
Expect(k8sClient.Delete(ctx, module)).Should(Succeed())
})
Expand Down Expand Up @@ -163,6 +176,9 @@ var _ = Describe("Module controller with Runner", func() {
RepoName: repo,
Path: path,
PlanOnly: &boolTrue,
Env: []corev1.EnvVar{
{Name: "TF_APPLIER_STRONGBOX_KEYRING", Value: string(sbKeyringData)},
},
},
}
Expect(k8sClient.Create(ctx, module)).Should(Succeed())
Expand Down Expand Up @@ -243,6 +259,7 @@ var _ = Describe("Module controller with Runner", func() {
{Name: "path", Value: testStateFilePath},
},
Env: []corev1.EnvVar{
{Name: "TF_APPLIER_STRONGBOX_KEYRING", Value: string(sbKeyringData)},
{Name: "TF_ENV_1", Value: "ENV-VALUE1"},
{Name: "TF_ENV_2", ValueFrom: &corev1.EnvVarSource{
ConfigMapKeyRef: &corev1.ConfigMapKeySelector{
Expand Down Expand Up @@ -335,6 +352,7 @@ var _ = Describe("Module controller with Runner", func() {
Expect(fetchedModule.Status.LastApplyInfo.Timestamp.UTC()).Should(Equal(fakeClock.T.UTC()))

// make sure all values are there in output
Expect(fetchedModule.Status.LastApplyInfo.Output).Should(ContainSubstring("TOP_SECRET_VALUE"))
Expect(fetchedModule.Status.LastApplyInfo.Output).Should(ContainSubstring("ENV-VALUE1"))
Expect(fetchedModule.Status.LastApplyInfo.Output).Should(ContainSubstring("ENV-VALUE2"))
Expect(fetchedModule.Status.LastApplyInfo.Output).Should(ContainSubstring("ENV-VALUE3"))
Expand Down Expand Up @@ -377,6 +395,9 @@ var _ = Describe("Module controller with Runner", func() {
RepoName: repo,
Path: path,
VaultRequests: &vaultReq,
Env: []corev1.EnvVar{
{Name: "TF_APPLIER_STRONGBOX_KEYRING", Value: string(sbKeyringData)},
},
},
}
Expect(k8sClient.Create(ctx, module)).Should(Succeed())
Expand Down
1 change: 1 addition & 0 deletions integration_test/src/modules/hello/.gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
secret.tf filter=strongbox diff=strongbox
1 change: 1 addition & 0 deletions integration_test/src/modules/hello/.strongbox-keyid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
JXm+9DGOuYW8d9/DE6VIuowv73JH7cn0vDDFkH/J1LA=
4 changes: 4 additions & 0 deletions integration_test/src/modules/hello/secret.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# STRONGBOX ENCRYPTED RESOURCE ; See https://github.com/uw-labs/strongbox
PCRo66xUEmoX3hq9xiNz10E/MS++r3DAbKFaysXt3DTgVJ7YxbxEZCQDooutzZKWaEz/KMxWB9Yf
K4bWOQyZ+XHNKlICQoo4OObKOF5fqAHAekRRb3jQuO2Jm2Iv5Hq0Xnhu9UCEffb7Q+3uxzCCOPao
Nbk=
2 changes: 1 addition & 1 deletion metrics/prometheus.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ type PrometheusInterface interface {
}

// Prometheus implements instrumentation of metrics for terraform-applier.
// terrafromExitCodeCount is a Counter vector to increment the number of exit codes for each terraform execution
// terraformExitCodeCount is a Counter vector to increment the number of exit codes for each terraform execution
// moduleRunCount is a Counter vector to increment the number of successful and failed run attempts for each module.
// moduleRunDuration is a Summary vector that keeps track of the duration for runs.
// moduleRunSuccess is the last run outcome of the module run.
Expand Down
65 changes: 65 additions & 0 deletions runner/strongbox.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package runner

import (
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
)

const strongboxKeyRingFile string = ".strongbox_keyring"

func ensureDecryption(ctx context.Context, cwd string, sbKeyringData string) error {

keyRingPath := filepath.Join(cwd, strongboxKeyRingFile)

// create strongbox keyRing file
if err := os.WriteFile(keyRingPath, []byte(sbKeyringData), 0600); err != nil {
return fmt.Errorf("error writing sb keyring file %w", err)
}

// setup env for strongbox command
runEnv := []string{
// HOME is also used to setup git config in current dir
fmt.Sprintf("HOME=%s", cwd),
//setup SB home for strongbox decryption
fmt.Sprintf("STRONGBOX_HOME=%s", cwd),
fmt.Sprintf("PATH=%s", os.Getenv("PATH")),
}

// setup git config via `strongbox -git-config`
if err := setupGitConfigForSB(ctx, cwd, runEnv); err != nil {
return err
}

return runStrongboxDecryption(ctx, cwd, runEnv, keyRingPath)
}

// setupGitConfigForSB will setup git filters to run strongbox
func setupGitConfigForSB(ctx context.Context, cwd string, runEnv []string) error {
s := exec.CommandContext(ctx, "strongbox", "-git-config")
s.Dir = cwd
s.Env = runEnv

stderr, err := s.CombinedOutput()
if err != nil {
return fmt.Errorf("error running strongbox -git-config err:%s ", stderr)
}

return nil
}

// runStrongboxDecryption will try to decrypt files in cwd using given keyRing file
func runStrongboxDecryption(ctx context.Context, cwd string, runEnv []string, keyringPath string) error {
s := exec.CommandContext(ctx, "strongbox", "-keyring", keyringPath, "-decrypt", "-recursive", cwd)
s.Dir = cwd
s.Env = runEnv

stderr, err := s.CombinedOutput()
if err != nil {
return fmt.Errorf("error running strongbox -decrypt err:%s ", stderr)
}

return nil
}
21 changes: 21 additions & 0 deletions runner/tfexec.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import (
"github.com/utilitywarehouse/terraform-applier/metrics"
)

const strongBoxEnv = "TF_APPLIER_STRONGBOX_KEYRING"

//go:generate go run github.com/golang/mock/mockgen -package runner -destination tfexec_mock.go github.com/utilitywarehouse/terraform-applier/runner TFExecuter

type TFExecuter interface {
Expand Down Expand Up @@ -66,16 +68,35 @@ func (r *Runner) NewTFRunner(
}

runEnv := make(map[string]string)
var strongboxKeyringData string

// first add Global ENV and then module ENVs
// this way user can override Global ENV if needed
for key := range r.GlobalENV {
runEnv[key] = r.GlobalENV[key]
}
for key := range envs {
// get SB keyring data if corresponding ENV is set
if key == strongBoxEnv {
strongboxKeyringData = envs[key]
continue
}
runEnv[key] = envs[key]
}

// Set HOME to cwd, this means that SSH should not pick up any
// HOME is also used to setup git config in current dir
runEnv["HOME"] = tmpWSDir
//setup SB home for terraform remote module
runEnv["STRONGBOX_HOME"] = tmpWSDir

if strongboxKeyringData != "" {
err := ensureDecryption(ctx, tmpWSDir, strongboxKeyringData)
if err != nil {
return nil, fmt.Errorf("unable to setup strongbox err:%w", err)
}
}

tf.SetEnv(runEnv)

// Setup *.auto.tfvars.json file to auto load TF variables during plan and apply
Expand Down