Skip to content

Commit

Permalink
Allow use of client public keys (#112)
Browse files Browse the repository at this point in the history
* Add publicKey to providerConfig API + first implementation

* Add first implementation without using a referenced resource

* Replace public key field with a reference to a secret

* Use v1beta1helper functions for fetching secret resource

* Format code

* Remove unused type Key

* Improve docs for client public keys

* Add a comment in the docs about kubeSystemManagerByGardener being the default scope

* Extract client key fetching to a seperate function

* Comment out unused function for verifying public keys in shoot admission

* Apply suggestions from code review

Co-authored-by: Vladimir Nachev <vladimir.nachev@sap.com>

* Split check for resource into two steps: existance & kind Secret

* Rename PublicKeysSecretReference to TrustedKeysResourceName

* Improve docs to include a description of the format of the keys

* Validate trusted keys resource in admission controller

* Create a first simple test for getClientKeys using fake client

* Format code

* Rename remaining places where old name of field was used & make gen

* Improve doc on feature

Co-authored-by: Vladimir Nachev <vladimir.nachev@sap.com>

* Improve tests for getClientKeys

* Fix typo 'date' -> 'data'

* Improve formatting

* Apply suggestions from PR

* Fix another 'date' typo

* Remove duplicate public keys from the final keys array that lakom uses for verification

* Add test for lakomConfig.Complete (duplicate keys removed)

* Improve uniqueKeys implementation (anonymous interface & slices.ContainsFunc)

* Run make generate

* Apply changes suggested by linters

* Run update skaffold deps

* Format tests

* Apply suggestions from code review

Co-authored-by: Vladimir Nachev <vladimir.nachev@sap.com>

* Fix the reuse of same slice between the tests

* Fix typo

* Remove unnecessary slice shallow copies

* Skip unnecessary checks in key validation if key name is empty

* Remove TODO about extracting the common logic from key validation

It was decided that the logic is simple enough and can remain as is.

---------

Co-authored-by: Vladimir Nachev <vladimir.nachev@sap.com>
  • Loading branch information
rrhubenov and vpnachev authored Jan 13, 2025
1 parent 7d128f5 commit f27e316
Show file tree
Hide file tree
Showing 15 changed files with 450 additions and 9 deletions.
5 changes: 5 additions & 0 deletions docs/usage/lakom.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ publicKeys:
-----END PUBLIC KEY-----
```
Here:
- `name` is logical human friendly name of the key.
- `algorithm` is the algorithm that has to be used to verify the signature, see [Supported RSA Signature Verification Algorithms](#supported-rsa-signature-verification-algorithms) for the list of supported algorithms.
- `key` is the cryptographic public key that will be used for image signature validation.

### Supported RSA Signature Verification Algorithms

- `RSASSA-PKCS1-v1_5-SHA256`: uses `RSASSA-PKCS1-v1_5` scheme with `SHA256` hash func
Expand Down
39 changes: 38 additions & 1 deletion docs/usage/shoot-extension.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,59 @@ In most of the Gardener setups the `shoot-lakom-service` extension is enabled gl
kind: Shoot
...
spec:
resources:
- name: lakom-ref
resourceRef:
apiVersion: v1
kind: Secret
name: lakom-secret
extensions:
- type: shoot-lakom-service
disabled: true
providerConfig:
apiVersion: lakom.extensions.gardener.cloud/v1alpha1
kind: LakomConfig
scope: KubeSystem
trustedKeysResourceName: lakom-ref
...
```

### Scope

The `scope` field instruct lakom which pods to validate. The possible values are:

- `KubeSystem`
Lakom will validate all pods in the `kube-system` namespace.
- `KubeSystemManagedByGardener`
Lakom will validate all pods in the `kube-system` namespace that are annotated with "managed-by/gardener"
Lakom will validate all pods in the `kube-system` namespace that are annotated with "managed-by/gardener". This is the default value.
- `Cluster`
Lakom will validate all pods in all namespaces.

### TrustedKeysResourceName

Lakom, by default, tries to verify only workloads that belong to Gardener. Because of this, the only public keys that it uses to do its job are the ones for the Gardener workload.

If you'd like to use Lakom as a tool for verifying your own workload, you'll need to add your own public keys to the ones that Lakom is already using. This can be achieved using Gardener [referenced resources](https://github.com/gardener/gardener/blob/master/docs/extensions/referenced-resources.md). More information about the keys and their format can be found [here](lakom.md#lakom-cosign-public-keys-configuration-file).

Simply:
1. Create a secret in your project namespace that contains a field `keys` with your keys as a value. Example keys:
```
- name: example-client-key1
algorithm: RSASSA-PSS-SHA256
key: |-
-----BEGIN PUBLIC KEY-----
MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAPeQXbIWMMXYV+9+j9b4jXTflnpfwn4E
GMrmqYVhm0sclXb2FPP5aV/NFH6SZdHDZcT8LCNsNgxzxV4N+UE/JIsCAwEAAQ==
-----END PUBLIC KEY-----
- name: example-client-key2
algorithm: RSASSA-PSS-SHA256
key: |-
-----BEGIN PUBLIC KEY-----
MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAPeQXbIWMMXYV+9+j9b4jXTflnpfwn4E
GMrmqYVhm0sclXb2FPP5aV/NFH6SZdHDZcT8LCNsNgxzxV4N+UE/JIsCAwEAAQ==
-----END PUBLIC KEY-----
```
2. Add a reference to your secret via the `resources` field in the shoot spec as shown above.
3. Add the name of your referenece in `trustedKeysResourceName ` in the provider config as shown above.

Now, whenever Lakom tries to verify a Pod, it will make sure to use your public keys as well.
12 changes: 12 additions & 0 deletions hack/api-reference/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,18 @@ github.com/gardener/gardener-extension-shoot-lakom-service/pkg/apis/lakom.ScopeT
<p>The scope in which lakom will verify pods</p>
</td>
</tr>
<tr>
<td>
<code>trustedKeysResourceName</code></br>
<em>
string
</em>
</td>
<td>
<em>(Optional)</em>
<p>TrustedKeysResourceName is the name of the shoot resource providing additional cosign public keys for image signature validation.</p>
</td>
</tr>
</tbody>
</table>
<hr/>
Expand Down
84 changes: 79 additions & 5 deletions pkg/admission/validator/lakom/shoot.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,18 @@ import (

"github.com/gardener/gardener-extension-shoot-lakom-service/pkg/apis/lakom"
"github.com/gardener/gardener-extension-shoot-lakom-service/pkg/constants"
"github.com/gardener/gardener-extension-shoot-lakom-service/pkg/lakom/config"
"github.com/gardener/gardener-extension-shoot-lakom-service/pkg/lakom/utils"

extensionswebhook "github.com/gardener/gardener/extensions/pkg/webhook"
"github.com/gardener/gardener/pkg/apis/core"
gardencorehelper "github.com/gardener/gardener/pkg/apis/core/helper"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/validation/field"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/yaml"
)

// shoot validates shoots
Expand Down Expand Up @@ -44,16 +50,78 @@ func findExtension(extensions []core.Extension, extensionType string) (int, core

func (s *shoot) validateScopeType(fldPath *field.Path, scopeType lakom.ScopeType) field.ErrorList {
errList := field.ErrorList{}

if !lakom.AllowedScopes.Has(scopeType) {
errList = append(errList, field.NotSupported(fldPath, scopeType, lakom.AllowedScopes.UnsortedList()))
}

return errList
}

func (s *shoot) validateCosignPublicKeys(fldPath *field.Path, cosignPublicKeys []config.Key) field.ErrorList {
errList := field.ErrorList{}

usedNames := map[string]any{}
for idx, k := range cosignPublicKeys {
if k.Name == "" {
errList = append(errList, field.Required(fldPath.Index(idx), "key name should no be empty"))
continue
}

if _, ok := usedNames[k.Name]; ok {
errList = append(errList, field.Duplicate(fldPath.Index(idx), k.Name))
}
usedNames[k.Name] = nil

if keys, err := utils.GetCosignPublicKeys([]byte(k.Key)); err != nil {
errList = append(errList, field.Invalid(fldPath.Index(idx), k.Key, fmt.Sprintf("key %s could not be parsed: %s", k.Name, err)))
} else if len(keys) != 1 {
errList = append(errList, field.Invalid(fldPath.Index(idx), k.Key, fmt.Sprintf("multiple keys with the name %s", k.Name)))
}
}

return errList
}

func (s *shoot) validateTrustedKeys(ctx context.Context, fldPath *field.Path, resourceName string, resources []core.NamedResourceReference, namespace string) field.ErrorList {
ref := gardencorehelper.GetResourceByName(resources, resourceName)
if ref == nil {
return field.ErrorList{field.Invalid(fldPath, resourceName, "there is no resource with this name in shoot.spec.resources")}
}
if ref.ResourceRef.Kind != "Secret" {
return field.ErrorList{field.Invalid(fldPath, resourceName, "resource must be of kind 'Secret'")}
}

secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: ref.ResourceRef.Name,
Namespace: namespace,
},
}

objectKey := client.ObjectKeyFromObject(secret)

// Explicitly use the client.Reader to prevent controller-runtime to start Informer for Secrets
// under the hood. The latter increases the memory usage of the component.
if err := s.apiReader.Get(ctx, objectKey, secret); err != nil {
return field.ErrorList{field.Invalid(fldPath, resourceName, fmt.Sprintf("failed to get secret %s, %s", objectKey, err.Error()))}
}

var keys []config.Key

rawKeys, ok := secret.Data["keys"]
if !ok {
return field.ErrorList{field.Invalid(fldPath, resourceName, fmt.Sprintf("could not get 'keys' in data from secret %s", objectKey))}
}

if err := yaml.UnmarshalStrict(rawKeys, &keys); err != nil {
return field.ErrorList{field.Invalid(fldPath, resourceName, fmt.Sprintf("failed to serialize keys from secret %s: %s", objectKey, err.Error()))}
}

return s.validateCosignPublicKeys(fldPath, keys)
}

// Validate validates the given shoot object
func (s *shoot) Validate(_ context.Context, new, _ client.Object) error {
func (s *shoot) Validate(ctx context.Context, new, _ client.Object) error {
shoot, ok := new.(*core.Shoot)
if !ok {
return fmt.Errorf("wrong object type %T, expected core.Shoot", new)
Expand All @@ -73,9 +141,15 @@ func (s *shoot) Validate(_ context.Context, new, _ client.Object) error {
if err := runtime.DecodeInto(s.decoder, lakomExt.ProviderConfig.Raw, lakomConfig); err != nil {
return fmt.Errorf("failed to decode providerConfig: %w", err)
}
if lakomConfig.Scope == nil {
return nil

allErrs := field.ErrorList{}

if lakomConfig.Scope != nil {
allErrs = append(allErrs, s.validateScopeType(providerConfigPath.Child("scope"), *lakomConfig.Scope)...)
}
if lakomConfig.TrustedKeysResourceName != nil {
allErrs = append(allErrs, s.validateTrustedKeys(ctx, providerConfigPath.Child("trustedKeysResourceName"), *lakomConfig.TrustedKeysResourceName, shoot.Spec.Resources, shoot.Namespace)...)
}

return s.validateScopeType(providerConfigPath.Child("scope"), *lakomConfig.Scope).ToAggregate()
return allErrs.ToAggregate()
}
2 changes: 2 additions & 0 deletions pkg/apis/lakom/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,6 @@ type LakomConfig struct {

// The scope in which lakom will verify pods
Scope *ScopeType
// TrustedKeysResourceName is the name of the shoot resource providing additional cosign public keys for image signature validation.
TrustedKeysResourceName *string
}
3 changes: 3 additions & 0 deletions pkg/apis/lakom/v1alpha1/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,7 @@ type LakomConfig struct {
// The scope in which lakom will verify pods
// +optional
Scope *lakom.ScopeType `json:"scope"`
// TrustedKeysResourceName is the name of the shoot resource providing additional cosign public keys for image signature validation.
// +optional
TrustedKeysResourceName *string `json:"trustedKeysResourceName,omitempty"`
}
2 changes: 2 additions & 0 deletions pkg/apis/lakom/v1alpha1/zz_generated.conversion.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions pkg/apis/lakom/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions pkg/apis/lakom/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

46 changes: 44 additions & 2 deletions pkg/controller/lifecycle/actuator.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ import (
"github.com/gardener/gardener/extensions/pkg/controller"
"github.com/gardener/gardener/extensions/pkg/controller/extension"
extensionssecretsmanager "github.com/gardener/gardener/extensions/pkg/util/secret/manager"
corev1beta1 "github.com/gardener/gardener/pkg/apis/core/v1beta1"
v1beta1constants "github.com/gardener/gardener/pkg/apis/core/v1beta1/constants"
v1beta1helper "github.com/gardener/gardener/pkg/apis/core/v1beta1/helper"
extensionsv1alpha1 "github.com/gardener/gardener/pkg/apis/extensions/v1alpha1"
resourcesv1alpha1 "github.com/gardener/gardener/pkg/apis/resources/v1alpha1"
"github.com/gardener/gardener/pkg/client/kubernetes"
Expand Down Expand Up @@ -153,18 +155,30 @@ func (a *actuator) Reconcile(ctx context.Context, logger logr.Logger, ex *extens
image.Tag = ptr.To[string](version.Get().GitVersion)
}

lakomConfig, err := yaml.JSONToYAML(a.serviceConfig.CosignPublicKeys.Raw)
gardenerPublicKeys, err := yaml.JSONToYAML(a.serviceConfig.CosignPublicKeys.Raw)
if err != nil {
return fmt.Errorf("failed to convert lakom config from json to yaml, %w", err)
}

var clientPublicKeys []byte
if lakomProviderConfig.TrustedKeysResourceName != nil {
var err error
clientPublicKeys, err = getClientKeys(ctx, a.client, cluster.Shoot.Spec.Resources, *lakomProviderConfig.TrustedKeysResourceName, namespace)
if err != nil {
return fmt.Errorf("failed to get the additional keys: %w", err)
}
}

lakomPublicKeys := gardenerPublicKeys
lakomPublicKeys = append(lakomPublicKeys, clientPublicKeys...)

seedResources, err := getSeedResources(
getLakomReplicas(controller.IsHibernationEnabled(cluster)),
namespace,
extensions.GenericTokenKubeconfigSecretNameFromCluster(cluster),
lakomShootAccessSecret.Secret.Name,
generatedSecrets[constants.WebhookTLSSecretName].Name,
string(lakomConfig),
string(lakomPublicKeys),
image.String(),
a.serviceConfig.UseOnlyImagePullSecrets,
a.serviceConfig.AllowUntrustedImages,
Expand Down Expand Up @@ -695,6 +709,34 @@ func getShootResources(webhookCaBundle []byte, extensionNamespace, shootAccessSe
return shootResources, nil
}

func getClientKeys(ctx context.Context, client client.Client, resources []corev1beta1.NamedResourceReference, resourceName, namespace string) ([]byte, error) {
ref := v1beta1helper.GetResourceByName(resources, resourceName)
if ref == nil {
return nil, fmt.Errorf("failed to find referenced resource with name %s", resourceName)
}
if ref.ResourceRef.Kind != "Secret" {
return nil, fmt.Errorf("references resource with name %s is not of kind 'Secret'", resourceName)
}

refSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: ref.ResourceRef.Name,
Namespace: namespace,
},
}

if err := controller.GetObjectByReference(ctx, client, &ref.ResourceRef, namespace, refSecret); err != nil {
return nil, fmt.Errorf("failed to read referenced secret %s%s for reference %s: %w", v1beta1constants.ReferencedResourcesPrefix, ref.ResourceRef.Name, resourceName, err)
}

clientKeys, ok := refSecret.Data["keys"]
if !ok {
return nil, fmt.Errorf("secret %s/%s is missing data key 'keys'", refSecret.Namespace, refSecret.Name)
}

return clientKeys, nil
}

func getRoleBinding(scope lakom.ScopeType, shootAccessServiceAccountName string) client.Object {
roleRef := rbacv1.RoleRef{
APIGroup: "rbac.authorization.k8s.io",
Expand Down
Loading

0 comments on commit f27e316

Please sign in to comment.