Skip to content
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
13 changes: 12 additions & 1 deletion docs/pages/reference/machine-id/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1336,8 +1336,19 @@ Configuration:
# destination, this will always be `kubernetes_secret`.
type: kubernetes_secret
# name specifies the name of the Kubernetes Secret to write the artifacts to.
# This must be in the same namespace that `tbot` is running in.
name: my-secret
# namespace specifies the Kubernetes namespace that the secret should be written
# to. If unspecified, this defaults to the value of the `POD_NAMESPACE`
# environment variable.
#
# When using the Helm chart, and specifying a namespace other than the one that
# `tbot` is running in, you must manually grant the `tbot` service account
# privileges to read and write to secrets in that namespace.
namespace:
# labels specifies the labels to apply to the Kubernetes Secret. This field is
# optional.
labels:
example: "foo"
```

## Bot resource
Expand Down
10 changes: 2 additions & 8 deletions lib/tbot/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -526,25 +526,19 @@ func DestinationFromURI(uriString string) (destination.Destination, error) {
}
return &destination.Memory{}, nil
case "kubernetes-secret":
if uri.Host != "" {
return nil, trace.BadParameter(
"kubernetes-secret scheme should not be specified with host",
)
}
if uri.Path == "" {
return nil, trace.BadParameter(
"kubernetes-secret scheme should have a path specified",
)
}
// kubernetes-secret:///my-secret
// TODO(noah): Eventually we'll support namespace in the host part of
// the URI. For now, we'll default to the namespace tbot is running in.

// Path will be prefixed with '/' so we'll strip it off.
secretName := strings.TrimPrefix(uri.Path, "/")

return &k8s.SecretDestination{
Name: secretName,
Name: secretName,
Namespace: uri.Host,
}, nil
default:
return nil, trace.BadParameter(
Expand Down
10 changes: 7 additions & 3 deletions lib/tbot/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,11 +169,15 @@ func TestDestinationFromURI(t *testing.T) {
},
},
{
in: "kubernetes-secret://my-secret",
in: "kubernetes-secret://my-secret",
wantErr: true,
},
{
in: "kubernetes-secret://my-namespace/my-secret",
want: &k8s.SecretDestination{
Name: "my-secret",
Name: "my-secret",
Namespace: "my-namespace",
},
wantErr: true,
},
}
for _, tt := range tests {
Expand Down
36 changes: 25 additions & 11 deletions lib/tbot/services/k8s/secret_destination.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,20 @@ type SecretDestination struct {
// When configured, these labels will overwrite any existing labels on the
// secret.
Labels map[string]string `yaml:"labels,omitempty"`
// Namespace to write the Kubernetes Secret to. If not specified, it
// defaults to the value of the POD_NAMESPACE environment variable.
//
// When using the Helm chart, you'll need to additionally grant the tbot
// service account permissions to read/write to the other namespace.
Namespace string `yaml:"namespace,omitempty"`

mu sync.Mutex
namespace string
k8s kubernetes.Interface
initialized bool
}

func (dks *SecretDestination) getSecret(ctx context.Context) (*corev1.Secret, error) {
secret, err := dks.k8s.CoreV1().Secrets(dks.namespace).Get(ctx, dks.Name, v1.GetOptions{})
secret, err := dks.k8s.CoreV1().Secrets(dks.Namespace).Get(ctx, dks.Name, v1.GetOptions{})
if err != nil {
return nil, trace.Wrap(err)
}
Expand All @@ -74,15 +79,15 @@ func (dks *SecretDestination) secretTemplate() *corev1.Secret {
Type: corev1.SecretTypeOpaque,
ObjectMeta: v1.ObjectMeta{
Name: dks.Name,
Namespace: dks.namespace,
Namespace: dks.Namespace,
Labels: dks.Labels,
},
Data: map[string][]byte{},
}
}

func (dks *SecretDestination) upsertSecret(ctx context.Context, secret *corev1.Secret, dryRun bool) error {
apply := applyconfigv1.Secret(dks.Name, dks.namespace).
apply := applyconfigv1.Secret(dks.Name, dks.Namespace).
WithData(secret.Data).
WithResourceVersion(secret.ResourceVersion).
WithType(secret.Type)
Expand All @@ -100,7 +105,7 @@ func (dks *SecretDestination) upsertSecret(ctx context.Context, secret *corev1.S
applyOpts.DryRun = []string{"All"}
}

_, err := dks.k8s.CoreV1().Secrets(dks.namespace).Apply(ctx, apply, applyOpts)
_, err := dks.k8s.CoreV1().Secrets(dks.Namespace).Apply(ctx, apply, applyOpts)
if err != nil {
return trace.Wrap(err)
}
Expand Down Expand Up @@ -132,9 +137,13 @@ func (dks *SecretDestination) Init(ctx context.Context, subdirs []string) error
return trace.BadParameter("destination has already been initialized")
}

if dks.namespace == "" {
dks.namespace = os.Getenv(kubernetesNamespaceEnv)
if dks.namespace == "" {
if dks.Namespace == "" {
log.DebugContext(
ctx,
"No explicit namespace provided for Kubernetes secret destination, attempting to detect from environment",
)
dks.Namespace = os.Getenv(kubernetesNamespaceEnv)
if dks.Namespace == "" {
return trace.BadParameter("unable to detect namespace from %s environment variable", kubernetesNamespaceEnv)
}
}
Expand Down Expand Up @@ -200,7 +209,7 @@ func (dks *SecretDestination) Write(ctx context.Context, name string, data []byt
ctx,
"Kubernetes secret missing on attempt to write data. One will be created.",
"secret_name", dks.Name,
"secret_namespace", dks.namespace,
"secret_namespace", dks.Namespace,
)
// If the secret doesn't exist, we create it on write - this is ensures
// that we can recover if the secret is deleted between renewal loops.
Expand Down Expand Up @@ -238,7 +247,7 @@ func (dks *SecretDestination) WriteMany(ctx context.Context, toWrite map[string]
ctx,
"Kubernetes secret missing on attempt to write data. One will be created.",
"secret_name", dks.Name,
"secret_namespace", dks.namespace,
"secret_namespace", dks.Namespace,
)
// If the secret doesn't exist, we create it on write - this is ensures
// that we can recover if the secret is deleted between renewal loops.
Expand Down Expand Up @@ -282,7 +291,12 @@ func (dks *SecretDestination) Read(ctx context.Context, name string) ([]byte, er
}

func (dks *SecretDestination) String() string {
return fmt.Sprintf("%s: %s", SecretDestinationType, dks.Name)
return fmt.Sprintf(
"%s: %s/%s",
SecretDestinationType,
dks.Namespace,
dks.Name,
)
}

func (dks *SecretDestination) MarshalYAML() (any, error) {
Expand Down
36 changes: 30 additions & 6 deletions lib/tbot/services/k8s/secret_destination_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ import (
)

func TestDestinationKubernetesSecret(t *testing.T) {
t.Setenv("POD_NAMESPACE", "test-namespace")
defaultNamespace := "test-namespace"
t.Setenv("POD_NAMESPACE", defaultNamespace)

// Hack a reactor into the Kubernetes client-go fake client set as it
// doesn't currently support Apply :)
Expand Down Expand Up @@ -68,8 +69,9 @@ func TestDestinationKubernetesSecret(t *testing.T) {
}

tests := []struct {
name string
dest *SecretDestination
name string
dest *SecretDestination
wantNamespace string

wantErr string
}{
Expand All @@ -79,6 +81,16 @@ func TestDestinationKubernetesSecret(t *testing.T) {
Name: "my-secret",
k8s: fakeClientSet(),
},
wantNamespace: defaultNamespace,
},
{
name: "no existing secret with explicit namespace",
dest: &SecretDestination{
Name: "my-secret",
Namespace: "my-other-namespace",
k8s: fake.NewClientset(),
},
wantNamespace: "my-other-namespace",
},
{
name: "labels",
Expand All @@ -90,6 +102,7 @@ func TestDestinationKubernetesSecret(t *testing.T) {
},
k8s: fakeClientSet(),
},
wantNamespace: defaultNamespace,
},
{
name: "existing secret",
Expand All @@ -102,6 +115,7 @@ func TestDestinationKubernetesSecret(t *testing.T) {
},
}),
},
wantNamespace: defaultNamespace,
},
}
for _, tt := range tests {
Expand Down Expand Up @@ -134,7 +148,9 @@ func TestDestinationKubernetesSecret(t *testing.T) {
require.Equal(t, []byte("data-d"), bData)

// Check labels have been set
secret, err := tt.dest.k8s.CoreV1().Secrets("test-namespace").Get(ctx, tt.dest.Name, metav1.GetOptions{})
secret, err := tt.dest.k8s.CoreV1().
Secrets(tt.wantNamespace).
Get(ctx, tt.dest.Name, metav1.GetOptions{})
require.NoError(t, err)
require.Equal(t, tt.dest.Labels, secret.Labels)
})
Expand Down Expand Up @@ -167,13 +183,21 @@ func TestDestinationKubernetesSecret_YAML(t *testing.T) {
{
name: "full",
in: &SecretDestination{
Name: "my-secret",
Name: "my-secret",
Namespace: "my-namespace",
Labels: map[string]string{
"key": "value",
},
},
},
}
testYAML(t, tests)
}

func TestDestinationKubernetesSecret_String(t *testing.T) {
require.Equal(t, "kubernetes_secret: my-secret", (&SecretDestination{Name: "my-secret"}).String())
require.Equal(
t,
"kubernetes_secret: foo/my-secret",
(&SecretDestination{Namespace: "foo", Name: "my-secret"}).String(),
)
}
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
type: kubernetes_secret
name: my-secret
labels:
key: value
namespace: my-namespace
Loading