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

Support age #296

Merged
merged 5 commits into from
Aug 14, 2024
Merged
Show file tree
Hide file tree
Changes from 4 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
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ RUN apk --no-cache add git gcc make musl-dev curl bash openssh-client
ENV \
KUBECTL_VERSION=v1.30.1 \
KUSTOMIZE_VERSION=v5.4.1 \
STRONGBOX_VERSION=1.1.0
STRONGBOX_VERSION=2.0.0-RC4

RUN os=$(go env GOOS) && arch=$(go env GOARCH) \
&& curl -Ls -o /usr/local/bin/kubectl https://storage.googleapis.com/kubernetes-release/release/${KUBECTL_VERSION}/bin/${os}/${arch}/kubectl \
Expand All @@ -27,7 +27,7 @@ RUN go get -t ./... \
&& make test \
&& CGO_ENABLED=0 && go build -o /kube-applier .

FROM alpine:3.17
FROM alpine:3.20
RUN apk --no-cache add git openssh-client tini
COPY templates/ /templates/
COPY static/ /static/
Expand Down
58 changes: 36 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,15 +82,16 @@ at first in order to bootstrap the kube-applier integration in a namespace.
#### Integration with `strongbox`

[strongbox](https://github.com/uw-labs/strongbox) is an encryption tool, geared
towards git repositories and working as a git filter.
towards Git repositories and working as a Git filter.

If `strongboxKeyringSecretRef` is defined in the Waybill spec (it is an object
that contains the attributes `name` and `namespace`), it should reference a
Secret resource which contains a key named `.strongbox_keyring` with its value
being a valid strongbox keyring file. That keyring is subsequently used when
applying the Waybill, allowing for decryption of files under the
`repositoryPath`. If the attribute `namespace` for `strongboxKeyringSecretRef`
is not specified then it defaults to the same namespace as the Waybill itself.
Secret resource which contains a key named `.strongbox_keyring` or
`.strongbox_identity` with the value being a valid Strongbox keyring or
identity file. That keyring/identity is subsequently used when applying the
Waybill, allowing for decryption of files under the `repositoryPath`. If the
attribute `namespace` for `strongboxKeyringSecretRef` is not specified then it
defaults to the same namespace as the Waybill itself.

This secret should be readable by the ServiceAccount of kube-applier. If
deployed using the provided kustomize bases, kube-applier's ServiceAccount will
Expand All @@ -104,22 +105,35 @@ the Secret should have an annotation called
all the namespaces that are allowed to use it.

For example, the following secret can be used by namespaces "ns-a", "ns-b" and
"ns-c":
"ns-c", assuming its deployed in `ns-a`:

```
kind: Secret
apiVersion: v1
metadata:
name: kube-applier-strongbox-keyring
namespace: ns-a
annotations:
kube-applier.io/allowed-namespaces: "ns-b, ns-c"
stringData:
.strongbox_keyring: |-
keyentries:
- description: mykey
key-id: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
key: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
```bash
# ./kustomization.yaml

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

secretGenerator:
- name: kube-applier-strongbox-keyring
files:
- .strongbox_keyring=strongbox-keyring
- .strongbox_identity=strongbox-identity
options:
annotations:
kube-applier.io/allowed-namespaces: "ns-b, ns-c"

# ./strongbox-keyring

keyentries:
- description: mykey
key-id: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
key: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

# ./strongbox-identity

# description: ident1
# public key: age1ex4ph3ryaathfac0xpjhxk50utn50mtprke7h0vsmdlh6j63q5dsafxehs
AGE-SECRET-KEY-1GNC98E3WNPAXE49FATT434CFC2THV5Q0SLW45T3VNYUVZ4F8TY6SREQR9Q
```

Each item in the list of allowed namespaces supports [shell pattern
Expand Down Expand Up @@ -399,7 +413,7 @@ $ make release VERSION=v3.3.3-rc.3

Copyright 2016 Box, Inc. All rights reserved.

Copyright (c) 2017-2023 Utility Warehouse Ltd.
Copyright (c) 2017-2024 Utility Warehouse Ltd.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down
2 changes: 2 additions & 0 deletions git/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,8 @@ func (r *Repository) CloneLocal(ctx context.Context, environment []string, dst,
return "", err
}

// Uncomment and use the following if running a Docker build with rootless Docker
//if _, err := r.runGitCommand(ctx, nil, "", "clone", "--no-checkout", "--no-hardlinks", r.path, dst); err != nil {
// git clone --no-checkout src dst
if _, err := r.runGitCommand(ctx, nil, "", "clone", "--no-checkout", r.path, dst); err != nil {
return "", err
Expand Down
2 changes: 1 addition & 1 deletion run/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,7 @@ func (r *Runner) setupRepositoryClone(ctx context.Context, waybill *kubeapplierv
repositoryPath = waybill.Namespace
}
subpath := filepath.Join(r.RepoPath, repositoryPath)
// Point strongbox home to the temporary home to be able to decrypt files based on waybill cnfiguratn
// Point Strongbox home to the temporary home to be able to decrypt files based on Waybill configuration
hash, err := r.Repository.CloneLocal(ctx, []string{fmt.Sprintf("STRONGBOX_HOME=%s", tmpHomeDir)}, tmpRepoDir, subpath)
if err != nil {
return "", "", err
Expand Down
216 changes: 216 additions & 0 deletions run/runner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -890,6 +890,222 @@ deployment.apps/test-deployment created
})
})

Context("When operating on a Waybill that defines a Strongbox identity", func() {
It("Should be able to apply encrypted files, given a Strongbox identity Secret", func() {
wbList := []*kubeapplierv1alpha1.Waybill{
{
TypeMeta: metav1.TypeMeta{APIVersion: "kube-applier.io/v1alpha1", Kind: "Waybill"},
ObjectMeta: metav1.ObjectMeta{
Name: "strongbox-age",
Namespace: "strongbox-age-missing",
},
Spec: kubeapplierv1alpha1.WaybillSpec{
AutoApply: ptr.To(true),
Prune: ptr.To(true),
RepositoryPath: "strongbox-age",
},
},
{
TypeMeta: metav1.TypeMeta{APIVersion: "kube-applier.io/v1alpha1", Kind: "Waybill"},
ObjectMeta: metav1.ObjectMeta{
Name: "strongbox-age",
Namespace: "strongbox-age-notfound",
},
Spec: kubeapplierv1alpha1.WaybillSpec{
AutoApply: ptr.To(true),
Prune: ptr.To(true),
StrongboxKeyringSecretRef: &kubeapplierv1alpha1.ObjectReference{Name: "invalid"},
},
},
{
TypeMeta: metav1.TypeMeta{APIVersion: "kube-applier.io/v1alpha1", Kind: "Waybill"},
ObjectMeta: metav1.ObjectMeta{
Name: "strongbox-age",
Namespace: "strongbox-age-empty",
},
Spec: kubeapplierv1alpha1.WaybillSpec{
AutoApply: ptr.To(true),
Prune: ptr.To(true),
StrongboxKeyringSecretRef: &kubeapplierv1alpha1.ObjectReference{Name: "strongbox-empty"},
},
},
{
TypeMeta: metav1.TypeMeta{APIVersion: "kube-applier.io/v1alpha1", Kind: "Waybill"},
ObjectMeta: metav1.ObjectMeta{
Name: "strongbox-age",
Namespace: "strongbox-age",
},
Spec: kubeapplierv1alpha1.WaybillSpec{
AutoApply: ptr.To(true),
Prune: ptr.To(true),
StrongboxKeyringSecretRef: &kubeapplierv1alpha1.ObjectReference{Name: "strongbox"},
},
},
{
TypeMeta: metav1.TypeMeta{APIVersion: "kube-applier.io/v1alpha1", Kind: "Waybill"},
ObjectMeta: metav1.ObjectMeta{
Name: "strongbox-age",
Namespace: "strongbox-age-strongbox-shared-not-allowed",
},
Spec: kubeapplierv1alpha1.WaybillSpec{
AutoApply: ptr.To(true),
Prune: ptr.To(true),
RepositoryPath: "strongbox-age",
StrongboxKeyringSecretRef: &kubeapplierv1alpha1.ObjectReference{Name: "strongbox", Namespace: "strongbox-age"},
},
},
{
TypeMeta: metav1.TypeMeta{APIVersion: "kube-applier.io/v1alpha1", Kind: "Waybill"},
ObjectMeta: metav1.ObjectMeta{
Name: "strongbox-age",
Namespace: "strongbox-age-strongbox-shared",
},
Spec: kubeapplierv1alpha1.WaybillSpec{
AutoApply: ptr.To(true),
Prune: ptr.To(true),
RepositoryPath: "strongbox-age",
StrongboxKeyringSecretRef: &kubeapplierv1alpha1.ObjectReference{Name: "strongbox", Namespace: "strongbox-age"},
},
},
{
TypeMeta: metav1.TypeMeta{APIVersion: "kube-applier.io/v1alpha1", Kind: "Waybill"},
ObjectMeta: metav1.ObjectMeta{
Name: "strongbox-age",
Namespace: "strongbox-age-strongbox-shared-is-allowed",
},
Spec: kubeapplierv1alpha1.WaybillSpec{
AutoApply: ptr.To(true),
Prune: ptr.To(true),
RepositoryPath: "strongbox-age",
StrongboxKeyringSecretRef: &kubeapplierv1alpha1.ObjectReference{Name: "strongbox", Namespace: "strongbox-age"},
},
},
}

testEnsureWaybills(wbList)

Expect(k8sClient.GetClient().Create(context.TODO(), &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "strongbox",
Namespace: "strongbox-age",
Annotations: map[string]string{secretAllowedNamespacesAnnotation: "strongbox-age-strongbox-shared,strongbox-age-strongbox-shared-is-*"},
},
StringData: map[string]string{
".strongbox_identity": `# description: ident1
# public key: age1ex4ph3ryaathfac0xpjhxk50utn50mtprke7h0vsmdlh6j63q5dsafxehs
AGE-SECRET-KEY-1GNC98E3WNPAXE49FATT434CFC2THV5Q0SLW45T3VNYUVZ4F8TY6SREQR9Q`,
},
Type: corev1.SecretTypeOpaque,
})).To(BeNil())
Expect(k8sClient.GetClient().Create(context.TODO(), &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "strongbox-empty",
Namespace: "strongbox-age-empty",
},
Type: corev1.SecretTypeOpaque,
})).To(BeNil())

headCommitHash, err := runner.Repository.HashForPath(context.TODO(), filepath.Join(runner.RepoPath, "strongbox-age"))
Expect(err).To(BeNil())
Expect(headCommitHash).ToNot(BeEmpty())

expectedStatus := []*kubeapplierv1alpha1.WaybillStatusRun{
{
Command: "",
Commit: headCommitHash,
ErrorMessage: "exit status 1",
Finished: metav1.Time{},
Output: `(?s)namespace/strongbox-age.*error:.*strongbox-age_repo_[\d]+.*invalid Yaml document separator: --BEGIN AGE ENCRYPTED FILE-----`,
Started: metav1.Time{},
Success: false,
Type: PollingRun.String(),
},
nil,
nil,
{
Command: "",
Commit: headCommitHash,
ErrorMessage: "",
Finished: metav1.Time{},
Output: `namespace/strongbox-age unchanged
deployment.apps/test-deployment created
`,
Started: metav1.Time{},
Success: true,
Type: PollingRun.String(),
},
nil,
{
Command: "",
Commit: headCommitHash,
ErrorMessage: "",
Finished: metav1.Time{},
Output: `namespace/strongbox-age unchanged
deployment.apps/test-deployment created
`,
Started: metav1.Time{},
Success: true,
Type: PollingRun.String(),
},
{
Command: "",
Commit: headCommitHash,
ErrorMessage: "",
Finished: metav1.Time{},
Output: `namespace/strongbox-age unchanged
deployment.apps/test-deployment created
`,
Started: metav1.Time{},
Success: true,
Type: PollingRun.String(),
},
}

// construct expected waybill list
expected := make([]kubeapplierv1alpha1.Waybill, len(wbList))
for i := range wbList {
expected[i] = *wbList[i]
expected[i].Status = kubeapplierv1alpha1.WaybillStatus{LastRun: expectedStatus[i]}
}

for i := range wbList {
Enqueue(runQueue, PollingRun, wbList[i])
}

Eventually(
func() error {
deployment := &appsv1.Deployment{}
return k8sClient.GetAPIReader().Get(context.TODO(), client.ObjectKey{Namespace: "strongbox-age", Name: "test-deployment"}, deployment)
},
time.Second*15,
time.Second,
).Should(BeNil())

testMatchEvents([]gomegatypes.GomegaMatcher{
matchEvent(*wbList[1], corev1.EventTypeWarning, "WaybillRunRequestFailed", `failed setting up repository clone: secrets "invalid" not found`),
matchEvent(*wbList[2], corev1.EventTypeWarning, "WaybillRunRequestFailed", `failed setting up repository clone: secret "strongbox-age-empty/strongbox-empty" does not contain key '.strongbox_keyring'`),
})

runner.Stop()

for i := range wbList {
if wbList[i].Status.LastRun != nil {
wbList[i].Status.LastRun.Output = testStripKubectlWarnings(wbList[i].Status.LastRun.Output)
}
Expect(*wbList[i]).Should(matchWaybill(expected[i], kubeCtlPath, "", runner.RepoPath, applyOptions.pruneWhitelist(wbList[i], runner.PruneBlacklist)))
}

testMetrics([]string{
`kube_applier_kubectl_exit_code_count{exit_code="1",namespace="strongbox-age-missing"} 1`,
`kube_applier_last_run_timestamp_seconds{namespace="strongbox-age"}`,
`kube_applier_namespace_apply_count{namespace="strongbox-age-missing",success="false"} 1`,
`kube_applier_namespace_apply_count{namespace="strongbox-age",success="true"} 1`,
`kube_applier_run_latency_seconds`,
`kube_applier_run_queue{namespace="strongbox-age",type="Git polling run"} 0`,
})
})
})

Context("When setting up the apply environment", func() {
It("Should properly validate the delegate Service Account secret", func() {
wbList := []*kubeapplierv1alpha1.Waybill{
Expand Down
18 changes: 13 additions & 5 deletions run/strongbox.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,20 @@ func (sb *strongboxBase) SetupStrongboxKeyring(ctx context.Context, kubeClient *
if err := checkSecretIsAllowed(waybill, secret); err != nil {
return err
}
strongboxData, ok := secret.Data[".strongbox_keyring"]
if !ok {
return fmt.Errorf(`secret "%s/%s" does not contain key '.strongbox_keyring'`, secret.Namespace, secret.Name)
keyring, ok1 := secret.Data[".strongbox_keyring"]
if ok1 {
if err := os.WriteFile(filepath.Join(homeDir, ".strongbox_keyring"), keyring, 0400); err != nil {
return err
}
}
if err := os.WriteFile(filepath.Join(homeDir, ".strongbox_keyring"), strongboxData, 0400); err != nil {
return err
identity, ok2 := secret.Data[".strongbox_identity"]
if ok2 {
if err := os.WriteFile(filepath.Join(homeDir, ".strongbox_identity"), identity, 0400); err != nil {
return err
}
}
if !ok1 && !ok2 {
return fmt.Errorf(`secret "%s/%s" does not contain key '.strongbox_keyring' or '.strongbox_identity'`, secret.Namespace, secret.Name)
}
return nil
}
Expand Down
1 change: 1 addition & 0 deletions testdata/manifests/strongbox-age/.gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
deployment.yaml filter=strongbox diff=strongbox
1 change: 1 addition & 0 deletions testdata/manifests/strongbox-age/.strongbox_recipient
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
age1ex4ph3ryaathfac0xpjhxk50utn50mtprke7h0vsmdlh6j63q5dsafxehs
4 changes: 4 additions & 0 deletions testdata/manifests/strongbox-age/00-namespace.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
kind: Namespace
apiVersion: v1
metadata:
name: strongbox-age
15 changes: 15 additions & 0 deletions testdata/manifests/strongbox-age/deployment.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB5TXhTQlVLNWtBZ0F4ejJV
eWIzdnFGbllmZ1M1Uk05bFhUYmdKYUxNQkdVClNwNkxmYnZYUnAxay83N2JyWWRK
dUY1VENnOWxCS044SStzc1FqdzUvclEKLS0tIE9Ob3NHUm1ZKzBUcU12RkJka05Z
ZWpRMjZiS2E0ZUxkTHJiaUR0VGF2dzAKUqQbb3PhczAZuM6emvAC1Xl55mGDlZEO
l7uELZcM88CN0vCZiEi6YC8hPJi2AhP6/8BQydwoR7uTyYxOVAd/1yw3ZgF1kZTJ
boK+LADYFma2iM77utRjKh9WmM8/2GDjEdHkTYXE/BPqXR405yx++MaDsx65J5U6
KknsfVP/GwwkKQzdOkY60MH5kpPixNDQjzgeYnwWfoSLDdocRmH3qvLEiVjXp+eS
weMhDO4V8HJ8ljpsorbMJaXVWzRDy6d1K+XKV5PqNdysaSmivsptlEGRV58fpidU
g5DyEW/rrRR5IrXutgXEzCnnst/nEEPGV1H8UeFXIhYuAxcF+gXZjDTzTwWhSgUJ
OYj1jOOTRj/c+wSMK3cUZvOLK4wV8maPJQFp6fd3KOUVnzT+4ANQA92o5il3nJG7
dAlWd2ddiRJfrSydrwXTKvHP7FZgVCVClIsQ1P3b9Sj9domQ8X5eMObGiMl2h2x4
9cH+qZzrkEhr4ZqhG/jHBK07xLqDaX7DwVHUSKR54EXtxYFKNuhMIKOFtmsAvCon
XJwuVDvvVvs8Lyj0
-----END AGE ENCRYPTED FILE-----
Loading