Support rotation of svc account tokens

This patchset introduces airshipctl command
- airshipctl cluster rotate-sa-token which basically rotates SA tokens

Previous work: https://review.opendev.org/#/c/749470/

Change-Id: Ibe932fa8d2831979e5b2ac2781f746e8ec2fdc3c
This commit is contained in:
Guhan Eswaran 2020-09-30 13:26:05 +00:00 committed by guhaneswaran20
parent 76e4d3f48c
commit b7dd46c4e6
16 changed files with 762 additions and 4 deletions

View File

@ -17,6 +17,7 @@ package cluster
import (
"github.com/spf13/cobra"
"opendev.org/airship/airshipctl/cmd/cluster/resetsatoken"
"opendev.org/airship/airshipctl/pkg/config"
)
@ -39,6 +40,7 @@ func NewClusterCommand(cfgFactory config.Factory) *cobra.Command {
clusterRootCmd.AddCommand(NewInitCommand(cfgFactory))
clusterRootCmd.AddCommand(NewMoveCommand(cfgFactory))
clusterRootCmd.AddCommand(NewStatusCommand(cfgFactory))
clusterRootCmd.AddCommand(resetsatoken.NewResetCommand(cfgFactory))
return clusterRootCmd
}

View File

@ -0,0 +1,76 @@
/*
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package resetsatoken
import (
"github.com/spf13/cobra"
"opendev.org/airship/airshipctl/pkg/cluster/resetsatoken"
"opendev.org/airship/airshipctl/pkg/config"
"opendev.org/airship/airshipctl/pkg/log"
)
const (
resetLong = `
Use to reset/rotate the Service Account(SA) tokens and additionally restart the
corresponding pods to get the latest token data reflected in the pod spec
Secret-namespace is a mandatory field and secret-name is optional. If secret-
name is not given, all the SA tokens in that particular namespace is considered,
else only that particular input secret-name`
resetExample = `
# To rotate a particular SA token
airshipctl cluster rotate-sa-token -n cert-manager -s cert-manager-token-vvn9p
# To rotate all the SA tokens in cert-manager namespace
airshipctl cluster rotate-sa-token -n cert-manager
`
)
// NewResetCommand creates a new command for generating secret information
func NewResetCommand(cfgFactory config.Factory) *cobra.Command {
r := &resetsatoken.ResetCommand{
Options: resetsatoken.ResetFlags{},
CfgFactory: cfgFactory,
}
resetCmd := &cobra.Command{
Use: "rotate-sa-token",
Short: "Rotate tokens of Service Accounts",
Long: resetLong[1:],
Example: resetExample,
RunE: func(cmd *cobra.Command, args []string) error {
return r.RunE()
},
}
resetCmd.Flags().StringVarP(&r.Options.Namespace, "secret-namespace", "n", "",
"namespace of the Service Account Token")
resetCmd.Flags().StringVarP(&r.Options.SecretName, "secret-name", "s", "",
"name of the secret containing Service Account Token")
resetCmd.Flags().StringVar(&r.Options.Kubeconfig, "kubeconfig", "",
"Path to kubeconfig associated with cluster being managed")
err := resetCmd.MarkFlagRequired("secret-namespace")
if err != nil {
log.Fatal(err)
}
err = resetCmd.MarkFlagRequired("kubeconfig")
if err != nil {
log.Fatalf("marking kubeconfig flag required failed: %v", err)
}
return resetCmd
}

View File

@ -0,0 +1,36 @@
/*
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package resetsatoken_test
import (
"testing"
"opendev.org/airship/airshipctl/cmd/cluster/resetsatoken"
"opendev.org/airship/airshipctl/testutil"
)
func TestResetToken(t *testing.T) {
cmdTests := []*testutil.CmdTest{
{
Name: "reset-with-help",
CmdLine: "--help",
Cmd: resetsatoken.NewResetCommand(nil),
},
}
for _, tt := range cmdTests {
testutil.RunTest(t, tt)
}
}

View File

@ -0,0 +1,24 @@
Use to reset/rotate the Service Account(SA) tokens and additionally restart the
corresponding pods to get the latest token data reflected in the pod spec
Secret-namespace is a mandatory field and secret-name is optional. If secret-
name is not given, all the SA tokens in that particular namespace is considered,
else only that particular input secret-name
Usage:
rotate-sa-token [flags]
Examples:
# To rotate a particular SA token
airshipctl cluster rotate-sa-token -n cert-manager -s cert-manager-token-vvn9p
# To rotate all the SA tokens in cert-manager namespace
airshipctl cluster rotate-sa-token -n cert-manager
Flags:
-h, --help help for rotate-sa-token
--kubeconfig string Path to kubeconfig associated with cluster being managed
-s, --secret-name string name of the secret containing Service Account Token
-n, --secret-namespace string namespace of the Service Account Token

View File

@ -5,10 +5,11 @@ Usage:
cluster [command]
Available Commands:
help Help about any command
init Deploy cluster-api provider components
move Move Cluster API objects, provider specific objects and all dependencies to the target cluster
status Retrieve statuses of deployed cluster components
help Help about any command
init Deploy cluster-api provider components
move Move Cluster API objects, provider specific objects and all dependencies to the target cluster
rotate-sa-token Rotate tokens of Service Accounts
status Retrieve statuses of deployed cluster components
Flags:
-h, --help help for cluster

View File

@ -26,5 +26,6 @@ such as getting status and deploying initial infrastructure.
* [airshipctl](airshipctl.md) - A unified entrypoint to various airship components
* [airshipctl cluster init](airshipctl_cluster_init.md) - Deploy cluster-api provider components
* [airshipctl cluster move](airshipctl_cluster_move.md) - Move Cluster API objects, provider specific objects and all dependencies to the target cluster
* [airshipctl cluster rotate-sa-token](airshipctl_cluster_rotate-sa-token.md) - Rotate tokens of Service Accounts
* [airshipctl cluster status](airshipctl_cluster_status.md) - Retrieve statuses of deployed cluster components

View File

@ -0,0 +1,49 @@
## airshipctl cluster rotate-sa-token
Rotate tokens of Service Accounts
### Synopsis
Use to reset/rotate the Service Account(SA) tokens and additionally restart the
corresponding pods to get the latest token data reflected in the pod spec
Secret-namespace is a mandatory field and secret-name is optional. If secret-
name is not given, all the SA tokens in that particular namespace is considered,
else only that particular input secret-name
```
airshipctl cluster rotate-sa-token [flags]
```
### Examples
```
# To rotate a particular SA token
airshipctl cluster rotate-sa-token -n cert-manager -s cert-manager-token-vvn9p
# To rotate all the SA tokens in cert-manager namespace
airshipctl cluster rotate-sa-token -n cert-manager
```
### Options
```
-h, --help help for rotate-sa-token
--kubeconfig string Path to kubeconfig associated with cluster being managed
-s, --secret-name string name of the secret containing Service Account Token
-n, --secret-namespace string namespace of the Service Account Token
```
### Options inherited from parent commands
```
--airshipconf string Path to file for airshipctl configuration. (default "$HOME/.airship/config")
--debug enable verbose output
```
### SEE ALSO
* [airshipctl cluster](airshipctl_cluster.md) - Manage Kubernetes clusters

View File

@ -0,0 +1,60 @@
/*
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package resetsatoken
import (
"opendev.org/airship/airshipctl/pkg/config"
"opendev.org/airship/airshipctl/pkg/k8s/client"
"opendev.org/airship/airshipctl/pkg/log"
)
// ResetFlags flags for reset command
type ResetFlags struct {
Namespace string
SecretName string
Kubeconfig string
}
// ResetCommand for reset command
type ResetCommand struct {
Options ResetFlags
CfgFactory config.Factory
}
// RunE implements the functionality for resetsatoken
func (c *ResetCommand) RunE() error {
airshipconfig, err := c.CfgFactory()
if err != nil {
return err
}
factory := client.DefaultClient
kclient, err := factory(airshipconfig.LoadedConfigPath(), c.Options.Kubeconfig)
if err != nil {
return err
}
manager, err := NewTokenManager(kclient.ClientSet())
if err != nil {
return err
}
log.Printf("Starting Token Rotation")
err = manager.RotateToken(c.Options.Namespace, c.Options.SecretName)
if err != nil {
return ErrRotateTokenFail{Err: err.Error()}
}
return nil
}

View File

@ -0,0 +1,85 @@
/*
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package resetsatoken_test
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"opendev.org/airship/airshipctl/pkg/cluster/resetsatoken"
"opendev.org/airship/airshipctl/pkg/config"
"opendev.org/airship/airshipctl/pkg/k8s/client"
"opendev.org/airship/airshipctl/pkg/k8s/client/fake"
)
func TestRunE(t *testing.T) {
airshipConfigPath := "testdata/airshipconfig.yaml"
kubeConfigPath := "testdata/kubeconfig.yaml"
tests := []struct {
testCaseName string
testErr string
resetFlags resetsatoken.ResetFlags
cfgFactory config.Factory
}{
{
testCaseName: "invalid config factory",
cfgFactory: func() (*config.Config, error) {
return nil, fmt.Errorf("test config error")
},
resetFlags: resetsatoken.ResetFlags{},
testErr: "test config error",
},
{
testCaseName: "valid config factory",
cfgFactory: config.CreateFactory(&airshipConfigPath),
resetFlags: resetsatoken.ResetFlags{
SecretName: "test-secret",
Namespace: "test-namespace",
},
testErr: "",
},
}
for _, tt := range tests {
t.Run(tt.testCaseName, func(t *testing.T) {
command := resetsatoken.ResetCommand{
Options: tt.resetFlags,
CfgFactory: tt.cfgFactory,
}
err := command.RunE()
if tt.testErr != "" {
assert.Contains(t, err.Error(), tt.testErr)
} else {
fakeConfig, err := command.CfgFactory()
assert.NoError(t, err)
factory := client.DefaultClient
_, err = factory(fakeConfig.LoadedConfigPath(), kubeConfigPath)
assert.NoError(t, err)
fakeClient := fake.NewClient()
assert.NotEmpty(t, fakeClient)
clientset := fakeClient.ClientSet()
fakeManager, err := resetsatoken.NewTokenManager(clientset)
assert.NoError(t, err)
err = fakeManager.RotateToken(command.Options.Namespace, command.Options.SecretName)
assert.Error(t, err)
}
})
}
}

View File

@ -0,0 +1,46 @@
/*
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package resetsatoken
import (
"fmt"
)
// ErrNoSATokenFound is returned if there are no SA tokens found in the provided namespace
type ErrNoSATokenFound struct {
namespace string
}
// ErrNotSAToken is returned if the user input is not an SA token
type ErrNotSAToken struct {
secretName string
}
// ErrRotateTokenFail is called when there is a failure in rotating the SA token
type ErrRotateTokenFail struct {
Err string
}
func (e ErrNoSATokenFound) Error() string {
return fmt.Sprintf("no service account tokens found in namespace %s", e.namespace)
}
func (e ErrNotSAToken) Error() string {
return fmt.Sprintf("%s is not a Service Account Token", e.secretName)
}
func (e ErrRotateTokenFail) Error() string {
return fmt.Sprintf("failed to rotate token: %s", e.Err)
}

View File

@ -0,0 +1,139 @@
/*
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package resetsatoken
import (
"fmt"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"opendev.org/airship/airshipctl/pkg/log"
)
const (
replicaSetKind = "ReplicaSet"
)
// TokenManager manages service account rotation
type TokenManager struct {
kclient kubernetes.Interface
}
// NewTokenManager returns an instance of a TokenManager
func NewTokenManager(kclient kubernetes.Interface) (*TokenManager, error) {
return &TokenManager{
kclient: kclient,
}, nil
}
// RotateToken - rotates token > 1. Deletes the secret and 2. Deletes its pod
// Deleting the SA Secret recreates a new secret with a new token information
// However, the pods referencing to the old secret needs to be refreshed
// manually and hence deleting the pod to allow it to get recreated with new
// secret reference
func (manager TokenManager) RotateToken(ns string, secretName string) error {
if secretName == "" {
return manager.rotateAllTokens(ns)
}
return manager.rotateSingleToken(ns, secretName)
}
// deleteSecret- deletes the secret
func (manager TokenManager) deleteSecret(secretName string, ns string) error {
return manager.kclient.CoreV1().Secrets(ns).Delete(secretName, &metav1.DeleteOptions{})
}
// deletePod - identifies the secret relationship with pods and deletes corresponding pods
// if its part of replicaset
func (manager TokenManager) deletePod(secretName string, ns string) error {
pods, err := manager.kclient.CoreV1().Pods(ns).List(metav1.ListOptions{})
if err != nil {
return err
}
for _, pod := range pods.Items {
for _, volume := range pod.Spec.Volumes {
if volume.Name == secretName {
if manager.isReplicaSet(pod.OwnerReferences) {
log.Printf("Deleting pod - %s in %s", pod.Name, ns)
if deleteErr := manager.kclient.CoreV1().Pods(ns).Delete(pod.Name,
&metav1.DeleteOptions{}); deleteErr != nil {
log.Printf("Failed to delete pod: %v", err.Error())
}
}
}
}
}
return nil
}
// rotateAllTokens rotates all the tokens in the given namespace
func (manager TokenManager) rotateAllTokens(ns string) error {
tokenTypeFieldSelector := fmt.Sprintf("type=%s", corev1.SecretTypeServiceAccountToken)
listOptions := metav1.ListOptions{FieldSelector: tokenTypeFieldSelector}
secrets, err := manager.kclient.CoreV1().Secrets(ns).List(listOptions)
if err != nil {
return err
}
if len(secrets.Items) == 0 {
return ErrNoSATokenFound{namespace: ns}
}
for _, secret := range secrets.Items {
err := manager.rotate(secret.Name, secret.Namespace)
if err != nil {
return err
}
}
return nil
}
// rotateSingleToken rotates a given token in the given ns
func (manager TokenManager) rotateSingleToken(ns string, secretName string) error {
secret, err := manager.kclient.CoreV1().Secrets(ns).Get(secretName, metav1.GetOptions{})
if err != nil {
return err
}
if secret.Type != corev1.SecretTypeServiceAccountToken {
return ErrNotSAToken{secretName: secretName}
}
return manager.rotate(secretName, ns)
}
// rotate performs delete action for secrets and its pods
func (manager TokenManager) rotate(secretName string, secretNamespace string) error {
log.Printf("Rotating token - %s in %s", secretName, secretNamespace)
err := manager.deleteSecret(secretName, secretNamespace)
if err != nil {
return err
}
return manager.deletePod(secretName, secretNamespace)
}
// isReplicaSet checks if the pod is controlled by a ReplicaSet making it safe to delete
func (manager TokenManager) isReplicaSet(ownerReferences []metav1.OwnerReference) bool {
for _, ownerRef := range ownerReferences {
if ownerRef.Kind == replicaSetKind {
return true
}
}
return false
}

View File

@ -0,0 +1,166 @@
/*
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package resetsatoken_test
import (
"io/ioutil"
"testing"
"github.com/stretchr/testify/assert"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/yaml"
kfake "k8s.io/client-go/kubernetes/fake"
"k8s.io/client-go/kubernetes/scheme"
ktesting "k8s.io/client-go/testing"
"opendev.org/airship/airshipctl/pkg/cluster/resetsatoken"
"opendev.org/airship/airshipctl/pkg/k8s/client/fake"
"opendev.org/airship/airshipctl/testutil"
)
type testCase struct {
name string
existingSecrets []*v1.Secret
existingPods []*v1.Pod
secretName string
secretNamespace string
numPodDeletes int
numSecretDeletes int
expectErr bool
}
var testCases = []testCase{
{
name: "no-pods-secrets",
expectErr: true,
},
{
name: "valid-secret-no-pod",
secretName: "valid-secret",
secretNamespace: "valid-namespace",
existingSecrets: []*v1.Secret{getSecret()},
numSecretDeletes: 1,
},
{
name: "valid-secret-no-pod-empty-filter",
secretNamespace: "valid-namespace",
existingSecrets: []*v1.Secret{getSecret()},
numSecretDeletes: 1,
},
{
name: "invalid-secret-no-pod",
secretName: "invalid-secret",
existingSecrets: []*v1.Secret{getSecret()},
secretNamespace: "valid-namespace",
},
{
name: "unmatched-secret-pod",
secretName: "invalid-secret",
secretNamespace: "valid-namespace",
existingPods: []*v1.Pod{getPod()},
existingSecrets: []*v1.Secret{getSecret()},
},
{
name: "matched-secret-pod",
secretName: "valid-secret",
secretNamespace: "valid-namespace",
existingPods: []*v1.Pod{getPod()},
existingSecrets: []*v1.Secret{getSecret()},
numPodDeletes: 1,
numSecretDeletes: 1,
},
}
func TestResetSaToken(t *testing.T) {
for _, testCase := range testCases {
cfg, _ := testutil.InitConfig(t)
var objects []runtime.Object
for _, pod := range testCase.existingPods {
objects = append(objects, pod)
}
for _, secret := range testCase.existingSecrets {
objects = append(objects, secret)
}
ra := fake.WithTypedObjects(objects...)
kclient := fake.NewClient(ra)
assert.NotEmpty(t, kclient)
assert.NotEmpty(t, cfg)
clientset := kclient.ClientSet()
manager, err := resetsatoken.NewTokenManager(clientset)
assert.NoError(t, err)
err = manager.RotateToken(testCase.secretNamespace, testCase.secretName)
if testCase.expectErr {
assert.Error(t, err)
continue
}
actions := clientset.(*kfake.Clientset).Actions()
podDeleteActions := filterActions(actions, "pods", "delete")
assert.Len(t, podDeleteActions, testCase.numPodDeletes)
secretDeleteActions := filterActions(actions, "secrets", "delete")
assert.Len(t, secretDeleteActions, testCase.numSecretDeletes)
}
}
func getSecret() *v1.Secret {
object := readObjectFromFile("testdata/secret.yaml")
if secret, ok := object.(*v1.Secret); ok {
return secret
}
return nil
}
func getPod() *v1.Pod {
object := readObjectFromFile("testdata/pod.yaml")
if pod, ok := object.(*v1.Pod); ok {
return pod
}
return nil
}
func readObjectFromFile(fileName string) runtime.Object {
contents, err := ioutil.ReadFile(fileName)
if err != nil {
return nil
}
jsonContents, err := yaml.ToJSON(contents)
if err != nil {
return nil
}
object, err := runtime.Decode(scheme.Codecs.UniversalDeserializer(), jsonContents)
if err != nil {
return nil
}
return object
}
func filterActions(actions []ktesting.Action, resource string, verb string) []ktesting.Action {
var result []ktesting.Action
for _, action := range actions {
if action.GetVerb() == verb && action.GetResource().Resource == resource {
result = append(result, action)
}
}
return result
}

View File

@ -0,0 +1,23 @@
apiVersion: airshipit.org/v1alpha1
contexts:
dummy_cluster:
contextKubeconf: dummycluster_ephemeral
manifest: dummy_manifest
currentContext: dummy_cluster
kind: Config
manifests:
dummy_manifest:
primaryRepositoryName: primary
repositories:
primary:
auth:
sshKey: testdata/test-key.pem
type: ssh-key
checkout:
branch: ""
force: false
remoteRef: ""
tag: v1.0.1
url: http://dummy.url.com/primary.git
subPath: site
targetPath: testdata

View File

@ -0,0 +1,19 @@
apiVersion: v1
clusters:
- cluster:
certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUN5RENDQWJDZ0F3SUJBZ0lCQURBTkJna3Foa2lHOXcwQkFRc0ZBREFWTVJNd0VRWURWUVFERXdwcmRXSmwKY201bGRHVnpNQjRYRFRFNU1Ea3lPVEUzTURNd09Wb1hEVEk1TURreU5qRTNNRE13T1Zvd0ZURVRNQkVHQTFVRQpBeE1LYTNWaVpYSnVaWFJsY3pDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBTUZyCkdxM0kyb2dZci81Y01Udy9Na1pORTNWQURzdEdyU240WjU2TDhPUGhMcUhDN2t1dno2dVpES3dCSGtGeTBNK2MKRXIzd2piUGE1aTV5NmkyMGtxSHBVMjdPZTA0dzBXV2s4N0RSZVlWaGNoZVJHRXoraWt3SndIcGRmMjJVemZNKwpkSDBzaUhuMVd6UnovYk4za3hMUzJlMnZ2U1Y3bmNubk1YRUd4OXV0MUY0NThHeWxxdmxXTUlWMzg5Q2didXFDCkcwcFdiMTBLM0RVZWdiT25Xa1FmSm5sTWRRVVZDUVdZZEZaaklrcWtkWi9hVTRobkNEV01oZXNWRnFNaDN3VVAKczhQay9BNWh1ZFFPbnFRNDVIWXZLdjZ5RjJWcDUyWExBRUx3NDJ4aVRKZlh0V1h4eHR6cU4wY1lyL2VxeS9XMQp1YVVGSW5xQjFVM0JFL1oxbmFrQ0F3RUFBYU1qTUNFd0RnWURWUjBQQVFIL0JBUURBZ0trTUE4R0ExVWRFd0VCCi93UUZNQU1CQWY4d0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQkFKUUVKQVBLSkFjVDVuK3dsWGJsdU9mS0J3c2gKZTI4R1c5R2QwM0N0NGF3RzhzMXE1ZHNua2tpZmVTUENHVFZ1SXF6UTZDNmJaSk9SMDMvVEl5ejh6NDJnaitDVApjWUZXZkltM2RKTnpRL08xWkdySXZZNWdtcWJtWDlpV0JaU24rRytEOGxubzd2aGMvY0tBRFR5OTMvVU92MThuCkdhMnIrRGJJcHcyTWVBVEl2elpxRS9RWlVSQ25DMmdjUFhTVzFqN2h4R3o1a3ZNcGVDZTdQYVUvdVFvblVHSWsKZ2t6ZzI4NHQvREhUUzc4N1V1SUg5cXBaV09yTFNMOGFBeUxQUHhWSXBteGZmbWRETE9TS2VUemRlTmxoSitUMwowQlBVaHBQTlJBNTNJN0hRQjhVUDR2elNONTkzZ1VFbVlFQ2Jic2RYSzB6ZVR6SDdWWHR2Zmd5WTVWWT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=
server: https://127.0.0.1:6443
name: dummycluster_ephemeral
contexts:
- context:
cluster: dummycluster_ephemeral
user: kubernetes-admin
name: dummy_cluster
current-context: dummy_cluster
kind: Config
preferences: {}
users:
- name: kubernetes-admin
user:
client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUM4akNDQWRxZ0F3SUJBZ0lJQXhEdzk2RUY4SXN3RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB4T1RBNU1qa3hOekF6TURsYUZ3MHlNREE1TWpneE56QXpNVEphTURReApGekFWQmdOVkJBb1REbk41YzNSbGJUcHRZWE4wWlhKek1Sa3dGd1lEVlFRREV4QnJkV0psY201bGRHVnpMV0ZrCmJXbHVNSUlCSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQXV6R0pZdlBaNkRvaTQyMUQKSzhXSmFaQ25OQWQycXo1cC8wNDJvRnpRUGJyQWd6RTJxWVZrek9MOHhBVmVSN1NONXdXb1RXRXlGOEVWN3JyLwo0K0hoSEdpcTVQbXF1SUZ5enpuNi9JWmM4alU5eEVmenZpa2NpckxmVTR2UlhKUXdWd2dBU05sMkFXQUloMmRECmRUcmpCQ2ZpS1dNSHlqMFJiSGFsc0J6T3BnVC9IVHYzR1F6blVRekZLdjJkajVWMU5rUy9ESGp5UlJKK0VMNlEKQlltR3NlZzVQNE5iQzllYnVpcG1NVEFxL0p1bU9vb2QrRmpMMm5acUw2Zkk2ZkJ0RjVPR2xwQ0IxWUo4ZnpDdApHUVFaN0hUSWJkYjJ0cDQzRlZPaHlRYlZjSHFUQTA0UEoxNSswV0F5bVVKVXo4WEE1NDRyL2J2NzRKY0pVUkZoCmFyWmlRd0lEQVFBQm95Y3dKVEFPQmdOVkhROEJBZjhFQkFNQ0JhQXdFd1lEVlIwbEJBd3dDZ1lJS3dZQkJRVUgKQXdJd0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQkFMMmhIUmVibEl2VHJTMFNmUVg1RG9ueVVhNy84aTg1endVWApSd3dqdzFuS0U0NDJKbWZWRGZ5b0hRYUM4Ti9MQkxyUXM0U0lqU1JYdmFHU1dSQnRnT1RRV21Db1laMXdSbjdwCndDTXZQTERJdHNWWm90SEZpUFl2b1lHWFFUSXA3YlROMmg1OEJaaEZ3d25nWUovT04zeG1rd29IN1IxYmVxWEYKWHF1TTluekhESk41VlZub1lQR09yRHMwWlg1RnNxNGtWVU0wVExNQm9qN1ZIRDhmU0E5RjRYNU4yMldsZnNPMAo4aksrRFJDWTAyaHBrYTZQQ0pQS0lNOEJaMUFSMG9ZakZxT0plcXpPTjBqcnpYWHh4S2pHVFVUb1BldVA5dCtCCjJOMVA1TnI4a2oxM0lrend5Q1NZclFVN09ZM3ltZmJobHkrcXZxaFVFa014MlQ1SkpmQT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=
client-key-data: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcFFJQkFBS0NBUUVBdXpHSll2UFo2RG9pNDIxREs4V0phWkNuTkFkMnF6NXAvMDQyb0Z6UVBickFnekUyCnFZVmt6T0w4eEFWZVI3U041d1dvVFdFeUY4RVY3cnIvNCtIaEhHaXE1UG1xdUlGeXp6bjYvSVpjOGpVOXhFZnoKdmlrY2lyTGZVNHZSWEpRd1Z3Z0FTTmwyQVdBSWgyZERkVHJqQkNmaUtXTUh5ajBSYkhhbHNCek9wZ1QvSFR2MwpHUXpuVVF6Rkt2MmRqNVYxTmtTL0RIanlSUkorRUw2UUJZbUdzZWc1UDROYkM5ZWJ1aXBtTVRBcS9KdW1Pb29kCitGakwyblpxTDZmSTZmQnRGNU9HbHBDQjFZSjhmekN0R1FRWjdIVEliZGIydHA0M0ZWT2h5UWJWY0hxVEEwNFAKSjE1KzBXQXltVUpVejhYQTU0NHIvYnY3NEpjSlVSRmhhclppUXdJREFRQUJBb0lCQVFDU0pycjlaeVpiQ2dqegpSL3VKMFZEWCt2aVF4c01BTUZyUjJsOE1GV3NBeHk1SFA4Vk4xYmc5djN0YUVGYnI1U3hsa3lVMFJRNjNQU25DCm1uM3ZqZ3dVQWlScllnTEl5MGk0UXF5VFBOU1V4cnpTNHRxTFBjM3EvSDBnM2FrNGZ2cSsrS0JBUUlqQnloamUKbnVFc1JpMjRzT3NESlM2UDE5NGlzUC9yNEpIM1M5bFZGbkVuOGxUR2c0M1kvMFZoMXl0cnkvdDljWjR5ZUNpNwpjMHFEaTZZcXJZaFZhSW9RRW1VQjdsbHRFZkZzb3l4VDR6RTE5U3pVbkRoMmxjYTF1TzhqcmI4d2xHTzBoQ2JyClB1R1l2WFFQa3Q0VlNmalhvdGJ3d2lBNFRCVERCRzU1bHp6MmNKeS9zSS8zSHlYbEMxcTdXUmRuQVhhZ1F0VzkKOE9DZGRkb0JBb0dCQU5NcUNtSW94REtyckhZZFRxT1M1ZFN4cVMxL0NUN3ZYZ0pScXBqd2Y4WHA2WHo0KzIvTAozVXFaVDBEL3dGTkZkc1Z4eFYxMnNYMUdwMHFWZVlKRld5OVlCaHVSWGpTZ0ZEWldSY1Z1Y01sNVpPTmJsbmZGCjVKQ0xnNXFMZ1g5VTNSRnJrR3A0R241UDQxamg4TnhKVlhzZG5xWE9xNTFUK1RRT1UzdkpGQjc1QW9HQkFPTHcKalp1cnZtVkZyTHdaVGgvRDNpWll5SVV0ZUljZ2NKLzlzbTh6L0pPRmRIbFd4dGRHUFVzYVd1MnBTNEhvckFtbgpqTm4vSTluUXd3enZ3MWUzVVFPbUhMRjVBczk4VU5hbk5TQ0xNMW1yaXZHRXJ1VHFnTDM1bU41eFZPdTUxQU5JCm4yNkFtODBJT2JDeEtLa0R0ZXJSaFhHd3g5c1pONVJCbG9VRThZNGJBb0dBQ3ZsdVhMZWRxcng5VkE0bDNoNXUKVDJXRVUxYjgxZ1orcmtRc1I1S0lNWEw4cllBTElUNUpHKzFuendyN3BkaEFXZmFWdVV2SDRhamdYT0h6MUs5aQpFODNSVTNGMG9ldUg0V01PY1RwU0prWm0xZUlXcWRiaEVCb1FGdUlWTXRib1BsV0d4ZUhFRHJoOEtreGp4aThSCmdEcUQyajRwY1IzQ0g5QjJ5a0lqQjVFQ2dZRUExc0xXLys2enE1c1lNSm14K1JXZThhTXJmL3pjQnVTSU1LQWgKY0dNK0wwMG9RSHdDaUU4TVNqcVN1ajV3R214YUFuanhMb3ZwSFlRV1VmUEVaUW95UE1YQ2VhRVBLOU4xbk8xMwp0V2lHRytIZkIxaU5PazFCc0lhNFNDbndOM1FRVTFzeXBaeEgxT3hueS9LYmkvYmEvWEZ5VzNqMGFUK2YvVWxrCmJGV1ZVdWtDZ1lFQTBaMmRTTFlmTjV5eFNtYk5xMWVqZXdWd1BjRzQxR2hQclNUZEJxdHFac1doWGE3aDdLTWEKeHdvamh5SXpnTXNyK2tXODdlajhDQ2h0d21sQ1p5QU92QmdOZytncnJ1cEZLM3FOSkpKeU9YREdHckdpbzZmTQp5aXB3Q2tZVGVxRThpZ1J6UkI5QkdFUGY4eVpjMUtwdmZhUDVhM0lRZmxiV0czbGpUemNNZVZjPQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo=

View File

@ -0,0 +1,25 @@
apiVersion: v1
kind: Pod
metadata:
name: valid-pod
namespace: valid-namespace
ownerReferences:
- apiVersion: apps/v1
blockOwnerDeletion: true
controller: true
kind: ReplicaSet
name: valid-pod-rs
spec:
containers:
- image: pod-image
volumeMounts:
- mountPath: /var/run/secrets/kubernetes.io/serviceaccount
name: valid-secret
readOnly: true
serviceAccount: valid-serviceaccount
serviceAccountName: valid-serviceaccount
volumes:
- name: valid-secret
secret:
defaultMode: 420
secretName: valid-secret

View File

@ -0,0 +1,6 @@
apiVersion: v1
kind: Secret
metadata:
name: valid-secret
namespace: valid-namespace
type: kubernetes.io/service-account-token