Merge pull request #28 from digitalocean/varsha/unused-secrets
Unused secrets: check if there are unused secrets in the cluster.varsha/versions
commit
1c5e8cbfec
14
checks.md
14
checks.md
|
@ -293,3 +293,17 @@ How to fix:
|
|||
```bash
|
||||
kubectl delete configmap <unused config map>
|
||||
```
|
||||
|
||||
###### Unused Secrets
|
||||
|
||||
Name: `unused-secret`
|
||||
|
||||
Group: `basic`
|
||||
|
||||
Description: This check reports all the secret names in the cluster that are not referenced by pods in the respective namespaces. The cluster can be cleaned up based on this information.
|
||||
|
||||
How to fix:
|
||||
|
||||
```bash
|
||||
kubectl delete secret <unused secret name>
|
||||
```
|
||||
|
|
|
@ -0,0 +1,135 @@
|
|||
package basic
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/digitalocean/clusterlint/checks"
|
||||
"github.com/digitalocean/clusterlint/kube"
|
||||
"golang.org/x/sync/errgroup"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
func init() {
|
||||
checks.Register(&unusedSecretCheck{})
|
||||
}
|
||||
|
||||
type unusedSecretCheck struct{}
|
||||
|
||||
type identifier struct {
|
||||
Name string
|
||||
Namespace string
|
||||
}
|
||||
|
||||
// Name returns a unique name for this check.
|
||||
func (s *unusedSecretCheck) Name() string {
|
||||
return "unused-secret"
|
||||
}
|
||||
|
||||
// Groups returns a list of group names this check should be part of.
|
||||
func (s *unusedSecretCheck) Groups() []string {
|
||||
return []string{"basic"}
|
||||
}
|
||||
|
||||
// Description returns a detailed human-readable description of what this check
|
||||
// does.
|
||||
func (s *unusedSecretCheck) Description() string {
|
||||
return "Checks if there are unused secrets in the cluster. Ignores service account tokens"
|
||||
}
|
||||
|
||||
// Run runs this check on a set of Kubernetes objects. It can return warnings
|
||||
// (low-priority problems) and errors (high-priority problems) as well as an
|
||||
// error value indicating that the check failed to run.
|
||||
func (s *unusedSecretCheck) Run(objects *kube.Objects) ([]checks.Diagnostic, error) {
|
||||
var diagnostics []checks.Diagnostic
|
||||
used, err := checkReferences(objects)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, secret := range filter(objects.Secrets.Items) {
|
||||
if _, ok := used[kube.Identifier{Name: secret.GetName(), Namespace: secret.GetNamespace()}]; !ok {
|
||||
secret := secret
|
||||
d := checks.Diagnostic{
|
||||
Severity: checks.Warning,
|
||||
Message: "Unused secret",
|
||||
Kind: checks.Secret,
|
||||
Object: &secret.ObjectMeta,
|
||||
Owners: secret.ObjectMeta.GetOwnerReferences(),
|
||||
}
|
||||
diagnostics = append(diagnostics, d)
|
||||
}
|
||||
}
|
||||
return diagnostics, nil
|
||||
}
|
||||
|
||||
//checkReferences checks each pod for config map references in volumes and environment variables
|
||||
func checkReferences(objects *kube.Objects) (map[kube.Identifier]struct{}, error) {
|
||||
used := make(map[kube.Identifier]struct{})
|
||||
var empty struct{}
|
||||
var mu sync.Mutex
|
||||
var g errgroup.Group
|
||||
for _, pod := range objects.Pods.Items {
|
||||
pod := pod
|
||||
namespace := pod.GetNamespace()
|
||||
g.Go(func() error {
|
||||
for _, volume := range pod.Spec.Volumes {
|
||||
s := volume.VolumeSource.Secret
|
||||
if s != nil {
|
||||
mu.Lock()
|
||||
used[kube.Identifier{Name: s.SecretName, Namespace: namespace}] = empty
|
||||
mu.Unlock()
|
||||
}
|
||||
if volume.VolumeSource.Projected != nil {
|
||||
for _, source := range volume.VolumeSource.Projected.Sources {
|
||||
s := source.Secret
|
||||
if s != nil {
|
||||
mu.Lock()
|
||||
used[kube.Identifier{Name: s.LocalObjectReference.Name, Namespace: namespace}] = empty
|
||||
mu.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, imageSecret := range pod.Spec.ImagePullSecrets {
|
||||
mu.Lock()
|
||||
used[kube.Identifier{Name: imageSecret.Name, Namespace: namespace}] = empty
|
||||
mu.Unlock()
|
||||
}
|
||||
identifiers := envVarsSecretRefs(pod.Spec.Containers, namespace)
|
||||
identifiers = append(identifiers, checkEnvVars(pod.Spec.InitContainers, namespace)...)
|
||||
mu.Lock()
|
||||
for _, i := range identifiers {
|
||||
used[i] = empty
|
||||
}
|
||||
mu.Unlock()
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
return used, g.Wait()
|
||||
}
|
||||
|
||||
// envVarsSecretRefs checks for config map references in container environment variables
|
||||
func envVarsSecretRefs(containers []corev1.Container, namespace string) []kube.Identifier {
|
||||
var refs []kube.Identifier
|
||||
for _, container := range containers {
|
||||
for _, env := range container.EnvFrom {
|
||||
if env.SecretRef != nil {
|
||||
refs = append(refs, kube.Identifier{Name: env.SecretRef.LocalObjectReference.Name, Namespace: namespace})
|
||||
}
|
||||
}
|
||||
}
|
||||
return refs
|
||||
}
|
||||
|
||||
// filter returns Secrets that are not of type `kubernetes.io/service-account-token`
|
||||
func filter(secrets []corev1.Secret) []corev1.Secret {
|
||||
var filtered []corev1.Secret
|
||||
for _, secret := range secrets {
|
||||
if secret.Type != corev1.SecretTypeServiceAccountToken {
|
||||
filtered = append(filtered, secret)
|
||||
}
|
||||
}
|
||||
return filtered
|
||||
}
|
|
@ -0,0 +1,175 @@
|
|||
package basic
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/digitalocean/clusterlint/checks"
|
||||
"github.com/digitalocean/clusterlint/kube"
|
||||
"github.com/stretchr/testify/assert"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
func TestUnusedSecretCheckMeta(t *testing.T) {
|
||||
unusedSecretCheck := unusedSecretCheck{}
|
||||
assert.Equal(t, "unused-secret", unusedSecretCheck.Name())
|
||||
assert.Equal(t, []string{"basic"}, unusedSecretCheck.Groups())
|
||||
assert.NotEmpty(t, unusedSecretCheck.Description())
|
||||
}
|
||||
|
||||
func TestUnusedSecretCheckRegistration(t *testing.T) {
|
||||
unusedSecretCheck := &unusedSecretCheck{}
|
||||
check, err := checks.Get("unused-secret")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, check, unusedSecretCheck)
|
||||
}
|
||||
|
||||
func TestUnusedSecretWarning(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
objs *kube.Objects
|
||||
expected []checks.Diagnostic
|
||||
}{
|
||||
{
|
||||
name: "no secrets",
|
||||
objs: &kube.Objects{Pods: &corev1.PodList{}, Secrets: &corev1.SecretList{}},
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "secret volume",
|
||||
objs: secretVolume(),
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "environment variable references secret",
|
||||
objs: secretEnvSource(),
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "pod with image pull secrets",
|
||||
objs: imagePullSecrets(),
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "projected volume references secret",
|
||||
objs: secretProjection(),
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "unused secret",
|
||||
objs: initSecret(),
|
||||
expected: []checks.Diagnostic{
|
||||
{
|
||||
Severity: checks.Warning,
|
||||
Message: "Unused secret",
|
||||
Kind: checks.Secret,
|
||||
Object: &metav1.ObjectMeta{Name: "secret_foo", Namespace: "k8s"},
|
||||
Owners: GetOwners(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
unusedSecretCheck := unusedSecretCheck{}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
d, err := unusedSecretCheck.Run(test.objs)
|
||||
assert.NoError(t, err)
|
||||
assert.ElementsMatch(t, test.expected, d)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func initSecret() *kube.Objects {
|
||||
objs := &kube.Objects{
|
||||
Pods: &corev1.PodList{
|
||||
Items: []corev1.Pod{
|
||||
{
|
||||
TypeMeta: metav1.TypeMeta{Kind: "Pod", APIVersion: "v1"},
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "pod_foo", Namespace: "k8s"},
|
||||
},
|
||||
},
|
||||
},
|
||||
Secrets: &corev1.SecretList{
|
||||
Items: []corev1.Secret{
|
||||
{
|
||||
TypeMeta: metav1.TypeMeta{Kind: "Secret", APIVersion: "v1"},
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "secret_foo", Namespace: "k8s"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
return objs
|
||||
}
|
||||
|
||||
func secretVolume() *kube.Objects {
|
||||
objs := initSecret()
|
||||
objs.Pods.Items[0].Spec = corev1.PodSpec{
|
||||
Volumes: []corev1.Volume{
|
||||
{
|
||||
Name: "bar",
|
||||
VolumeSource: corev1.VolumeSource{
|
||||
Secret: &corev1.SecretVolumeSource{
|
||||
SecretName: "secret_foo",
|
||||
},
|
||||
},
|
||||
}},
|
||||
}
|
||||
return objs
|
||||
}
|
||||
|
||||
func secretProjection() *kube.Objects {
|
||||
objs := initSecret()
|
||||
objs.Pods.Items[0].Spec = corev1.PodSpec{
|
||||
Volumes: []corev1.Volume{
|
||||
{
|
||||
Name: "bar",
|
||||
VolumeSource: corev1.VolumeSource{
|
||||
Projected: &corev1.ProjectedVolumeSource{
|
||||
Sources: []corev1.VolumeProjection{
|
||||
{
|
||||
Secret: &corev1.SecretProjection{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: "secret_foo",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}},
|
||||
}
|
||||
return objs
|
||||
}
|
||||
|
||||
func secretEnvSource() *kube.Objects {
|
||||
objs := initSecret()
|
||||
objs.Pods.Items[0].Spec = corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "test-container",
|
||||
Image: "docker.io/nginx",
|
||||
EnvFrom: []corev1.EnvFromSource{
|
||||
{
|
||||
SecretRef: &corev1.SecretEnvSource{
|
||||
LocalObjectReference: corev1.LocalObjectReference{Name: "secret_foo"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}},
|
||||
}
|
||||
return objs
|
||||
}
|
||||
|
||||
func imagePullSecrets() *kube.Objects {
|
||||
objs := initSecret()
|
||||
objs.Pods.Items[0].Spec = corev1.PodSpec{
|
||||
ImagePullSecrets: []corev1.LocalObjectReference{
|
||||
{
|
||||
Name: "secret_foo",
|
||||
},
|
||||
},
|
||||
}
|
||||
return objs
|
||||
}
|
Loading…
Reference in New Issue