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

[v16] operator: support storing SSO connector client secret in a Kubernetes Secret #46902

Merged
merged 11 commits into from
Sep 25, 2024
1 change: 1 addition & 0 deletions docs/cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -910,6 +910,7 @@
"teleportdevname",
"teleportdevprotocol",
"teleporters",
"teleportgithubconnector",
"teleportinfra",
"teleportopensshserverv",
"teleportproxy",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,17 @@ finalizer or remove the ignore annotation.

Possible values are `"true"` or `"false"` (those are strings, as Booleans are not valid label values in Kubernetes).

### Look up values from secrets

Some Teleport resources might contain sensitive values. Select CR fields can reference an existing
Kubernetes secret and the operator will retrieve the value from the secret when reconciling.

Even when you store sensitive values out of CRs, the CRs must still be considered as critical as
the Kubernetes secrets themselves. Many CRs configure Teleport RBAC. Someone with CR editing permissions can become a
Teleport administrator and retrieve the sensitive values from Teleport.

See [the dedicated guide](./teleport-operator/secret-lookup.mdx) for more details.

### Troubleshooting

(!docs/pages/includes/diagnostics/kubernetes-operator-troubleshooting.mdx!)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
---
title: Looking up values from secrets
description: How to store sensitive values in a Kubernetes Secret and have the operator look them up.
---

This guide describes how to store sensitive information in Kubernetes Secrets instead
of the Teleport Kubernetes operator CRs.

## How it works

Some Teleport resources might contain sensitive values. Select CR fields can reference an existing
Kubernetes secret and the operator will retrieve the value from the secret when reconciling.

Currently only the GithubConnector and OIDCConnector `client_secret` field support secret lookup.

## Prerequisites

To follow this guide you need:

- A running Teleport cluster
- [A functional Teleport Kubernetes operator setup](../teleport-operator.mdx#setting-up-the-operator)
- Kubernetes rights to edit CRs and Secrets in the operator namespace
- `kubectl` installed locally and configured for your Kubernetes cluster
- A working GitHub or OIDC connector you want to manage with the operator
- `tctl` and `tsh` installed and logged in the Teleport cluster

## Important Considerations

Even when you store sensitive values out of CRs, the CRs must still be considered as critical as
the Kubernetes secrets themselves. Many CRs configure Teleport RBAC. Someone with permissions to edit CRs can become a
Teleport administrator and retrieve the sensitive values from Teleport.

The secret lookup feature has two limitations you must take into account before configuring it:
- for performance reasons, the secret is not watched. A secret content change is not immediately reflected on
the resource. To force the operator to use the new secret value, you must trigger a reconciliation by editing the CR,
restarting the operator, or waiting for the next full sync (every 12 hours).
- for security reasons, the operator doesn't allow lookup from arbitrary secrets. The secret must be annotated with
`resources.teleport.dev/allow-lookup-from-cr`. Possible values are `*`, or a comma-separated list of CR names.

## Step 1/3. Create a Kubernetes Secret containing the sensitive value

For this guide, the sensitive value we want to store is the GitHub connector client secret.

Create the following `secret.yaml` manifest:

```yaml
apiVersion: v1
kind: Secret
metadata:
name: teleport-github-connector
annotations:
# This annotation allows any CR to look up this secret
resources.teleport.dev/allow-lookup-from-cr: "*"
# We use stringData instead of data for the sake of simplicity, both are OK
stringData:
githubSecret: my-github-secret-value
```

If <Var name="teleport-iac" /> is your Teleport Kubernetes operator namespace, apply the manifest using `kubectl`:

```code
$ kubectl apply -n <Var name="teleport-iac" /> -f secret.yaml
secret/teleport-github-connector created
```

## Step 2/3. Create a custom resource referencing the secret

Create the following `github-connector.yaml` manifest:

```yaml
apiVersion: resources.teleport.dev/v3
kind: TeleportGithubConnector
metadata:
name: github
spec:
# This value will be looked up from the secret. `teleport-github-connector` is the secret name and `githubSecret` is the secret key.
client_secret: "secret://teleport-github-connector/githubSecret"
# Replace all the values below by the ones to work with your github account
client_id: my-client-id
display: Github
redirect_url: "my value"
teams_to_roles:
- organization: ORG-NAME
roles:
- access
team: team-name
```

Apply the manifest in the same namespace as the operator and the secret:

```code
$ kubectl apply -n <Var name="teleport-iac" /> -f github-connector.yaml
teleportgithubconnector.resources.teleport.dev/github created
```

## Step 3/3. Validate the resource was created

The operator indicates if the reconciliation worked on the CR `status` field.
Run the following command to know if it worked:

```code
$ kubectl get -n -n <Var name="teleport-iac" /> teleportgithubconnector github -o yaml

apiVersion: resources.teleport.dev/v3
kind: TeleportGithubConnector
# [...]
status:
conditions:
- lastTransitionTime: "2022-07-25T16:15:52Z"
message: Teleport resource has the Kubernetes origin label.
reason: OriginLabelMatching
status: "True"
type: TeleportResourceOwned
- lastTransitionTime: "2022-07-25T17:08:58Z"
message: 'Teleport Resource was successfully reconciled, no error was returned by Teleport.'
reason: NoError
status: "True"
type: SuccessfullyReconciled
```

If everything worked, all condition statuses should be `True`. Of some status is `False`, the message and the reason
will give you more information about what failed.

Finally, validate the resource has been properly created in Teleport:
```code
$ tctl get github
version: v3
kind: github
metadata:
name: github
spec:
client_secret: "my-github-secret-value"
# ...
```

You should see that the content of `spec.client_secret` has been replaced by the secret's content.

Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ resource, which you can apply after installing the Teleport Kubernetes operator.
|api_endpoint_url|string|APIEndpointURL is the URL of the API endpoint of the Github instance this connector is for.|
|client_id|string|ClientID is the Github OAuth app client ID.|
|client_redirect_settings|[object](#specclient_redirect_settings)|ClientRedirectSettings defines which client redirect URLs are allowed for non-browser SSO logins other than the standard localhost ones.|
|client_secret|string|ClientSecret is the Github OAuth app client secret.|
|client_secret|string|ClientSecret is the Github OAuth app client secret. This field supports secret lookup. See the operator documentation for more details.|
|display|string|Display is the connector display name.|
|endpoint_url|string|EndpointURL is the URL of the GitHub instance this connector is for.|
|redirect_url|string|RedirectURL is the authorization callback URL.|
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ resource, which you can apply after installing the Teleport Kubernetes operator.
|claims_to_roles|[][object](#specclaims_to_roles-items)|ClaimsToRoles specifies a dynamic mapping from claims to roles.|
|client_id|string|ClientID is the id of the authentication client (Teleport Auth server).|
|client_redirect_settings|[object](#specclient_redirect_settings)|ClientRedirectSettings defines which client redirect URLs are allowed for non-browser SSO logins other than the standard localhost ones.|
|client_secret|string|ClientSecret is used to authenticate the client.|
|client_secret|string|ClientSecret is used to authenticate the client. This field supports secret lookup. See the operator documentation for more details.|
|display|string|Display is the friendly name for this provider.|
|google_admin_email|string|GoogleAdminEmail is the email of a google admin to impersonate.|
|google_service_account|string|GoogleServiceAccount is a string containing google service account credentials.|
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,9 @@ spec:
type: array
type: object
client_secret:
description: ClientSecret is the Github OAuth app client secret.
description: ClientSecret is the Github OAuth app client secret. This
field supports secret lookup. See the operator documentation for
more details.
type: string
display:
description: Display is the connector display name.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,9 @@ spec:
type: array
type: object
client_secret:
description: ClientSecret is used to authenticate the client.
description: ClientSecret is used to authenticate the client. This
field supports secret lookup. See the operator documentation for
more details.
type: string
display:
description: Display is the friendly name for this provider.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ metadata:
name: {{ include "teleport-cluster.operator.fullname" . }}
namespace: {{ .Release.Namespace }}
rules:
# Rights to manage the Teleport CRs
- apiGroups:
- "resources.teleport.dev"
resources:
Expand Down Expand Up @@ -41,6 +42,7 @@ rules:
- patch
- update
- watch
# Used to perform leader election when running with multiple replicas
- apiGroups:
- "coordination.k8s.io"
resources:
Expand All @@ -49,11 +51,19 @@ rules:
- create
- get
- update
# Ability to emit reconciliation events
- apiGroups:
- ""
resources:
- events
verbs:
- create
# Ability to lookup sensitive values from secrets rather than CRs
- apiGroups:
- ""
resources:
- "secrets"
verbs:
- "get"
{{- end -}}
{{- end -}}
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,12 @@ tests:
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
name: RELEASE-NAME-operator

- it: grants access to secret in the namespace
asserts:
- contains:
path: rules
content:
apiGroups: [""]
resources: ["secrets"]
verbs: ["get"]
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,9 @@ spec:
type: array
type: object
client_secret:
description: ClientSecret is the Github OAuth app client secret.
description: ClientSecret is the Github OAuth app client secret. This
field supports secret lookup. See the operator documentation for
more details.
type: string
display:
description: Display is the connector display name.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,9 @@ spec:
type: array
type: object
client_secret:
description: ClientSecret is used to authenticate the client.
description: ClientSecret is used to authenticate the client. This
field supports secret lookup. See the operator documentation for
more details.
type: string
display:
description: Display is the friendly name for this provider.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,13 @@ import (
resourcesv3 "github.com/gravitational/teleport/integrations/operator/apis/resources/v3"
"github.com/gravitational/teleport/integrations/operator/controllers"
"github.com/gravitational/teleport/integrations/operator/controllers/reconcilers"
"github.com/gravitational/teleport/integrations/operator/controllers/resources/secretlookup"
)

// githubConnectorClient implements TeleportResourceClient and offers CRUD methods needed to reconcile github_connectors
type githubConnectorClient struct {
teleportClient *client.Client
kubeClient kclient.Client
}

// Get gets the Teleport github_connector of a given name
Expand All @@ -59,10 +61,23 @@ func (r githubConnectorClient) Delete(ctx context.Context, name string) error {
return trace.Wrap(r.teleportClient.DeleteGithubConnector(ctx, name))
}

func (r githubConnectorClient) Mutate(ctx context.Context, new, _ types.GithubConnector, crKey kclient.ObjectKey) error {
secret := new.GetClientSecret()
if secretlookup.IsNeeded(secret) {
resolvedSecret, err := secretlookup.Try(ctx, r.kubeClient, crKey.Name, crKey.Namespace, secret)
if err != nil {
return trace.Wrap(err)
}
new.SetClientSecret(resolvedSecret)
}
return nil
}

// NewGithubConnectorReconciler instantiates a new Kubernetes controller reconciling github_connector resources
func NewGithubConnectorReconciler(client kclient.Client, tClient *client.Client) (controllers.Reconciler, error) {
githubClient := &githubConnectorClient{
teleportClient: tClient,
kubeClient: client,
}

resourceReconciler, err := reconcilers.NewTeleportResourceWithoutLabelsReconciler[types.GithubConnector, *resourcesv3.TeleportGithubConnector](
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,15 @@ import (

"github.com/google/go-cmp/cmp"
"github.com/gravitational/trace"
"github.com/stretchr/testify/require"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
kclient "sigs.k8s.io/controller-runtime/pkg/client"

"github.com/gravitational/teleport/api/types"
resourcesv3 "github.com/gravitational/teleport/integrations/operator/apis/resources/v3"
"github.com/gravitational/teleport/integrations/operator/controllers/reconcilers"
"github.com/gravitational/teleport/integrations/operator/controllers/resources/secretlookup"
"github.com/gravitational/teleport/integrations/operator/controllers/resources/testlib"
)

Expand Down Expand Up @@ -136,3 +139,51 @@ func TestGithubConnectorUpdate(t *testing.T) {
test := &githubTestingPrimitives{}
testlib.ResourceUpdateTest[types.GithubConnector, *resourcesv3.TeleportGithubConnector](t, test)
}

func TestGithubConnectorSecretLookup(t *testing.T) {
test := &githubTestingPrimitives{}
setup := testlib.SetupTestEnv(t)
test.Init(setup)
ctx := context.Background()

crName := validRandomResourceName("github")
secretName := validRandomResourceName("github-secret")
secretKey := "client-secret"
secretValue := validRandomResourceName("secret-value")

secret := &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: secretName,
Namespace: setup.Namespace.Name,
Annotations: map[string]string{
secretlookup.AllowLookupAnnotation: crName,
},
},
StringData: map[string]string{
secretKey: secretValue,
},
Type: v1.SecretTypeOpaque,
}
kubeClient := setup.K8sClient
require.NoError(t, kubeClient.Create(ctx, secret))

github := &resourcesv3.TeleportGithubConnector{
ObjectMeta: metav1.ObjectMeta{
Name: crName,
Namespace: setup.Namespace.Name,
},
Spec: resourcesv3.TeleportGithubConnectorSpec(githubSpec),
}

github.Spec.ClientSecret = "secret://" + secretName + "/" + secretKey

require.NoError(t, kubeClient.Create(ctx, github))

testlib.FastEventually(t, func() bool {
gh, err := test.GetTeleportResource(ctx, crName)
if err != nil {
return false
}
return gh.GetClientSecret() == secretValue
})
}
Loading
Loading