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

feat: Support cosign --k8s-keychain flag #551

Merged
merged 1 commit into from Feb 21, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
28 changes: 21 additions & 7 deletions connaisseur/res/config_schema.json
Expand Up @@ -148,14 +148,28 @@
},
"auth": {
"type": "object",
"properties": {
"secret_name": {
"type": "string",
"pattern": "[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*"
"oneOf": [
{
"properties": {
"secret_name": {
"type": "string",
"pattern": "[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*"
}
},
"required": [
"secret_name"
]
},
{
"properties": {
"k8s_keychain": {
"type": "boolean"
}
},
"required": [
"k8s_keychain"
]
}
},
"required": [
"secret_name"
]
},
"cert": {
Expand Down
5 changes: 4 additions & 1 deletion connaisseur/validators/cosign/cosign_validator.py
Expand Up @@ -21,10 +21,12 @@
class CosignValidator(ValidatorInterface):
name: str
trust_roots: list
k8s_keychain: bool

def __init__(self, name: str, trust_roots: list, **kwargs):
def __init__(self, name: str, trust_roots: list, auth: dict = None, **kwargs):
super().__init__(name, **kwargs)
self.trust_roots = trust_roots
self.k8s_keychain = False if auth is None else auth.get("k8s_keychain", False)

def __get_key(self, key_name: str = None):
key_name = key_name or "default"
Expand Down Expand Up @@ -137,6 +139,7 @@ def __invoke_cosign(self, image, key):
"--output",
"text",
*pubkey_config,
*(["--k8s-keychain"] if self.k8s_keychain else []),
image,
]

Expand Down
36 changes: 32 additions & 4 deletions docs/validators/sigstore_cosign.md
Expand Up @@ -94,8 +94,9 @@ kubectl run altsigned --image=docker.io/securesystemsengineering/testimage:co-si
| `trust_roots[*].name` | | :heavy_check_mark: | See [basics](../basics.md#validators). |
| `trust_roots[*].key` | | :heavy_check_mark: | See [basics](../basics.md#validators). ECDSA public key from `cosign.pub` file or [KMS URI](https://github.com/sigstore/cosign/blob/main/KMS.md). See additional notes [below](#kms-support). |
| `host` | | | Not yet implemented. |
| `auth.` | | | Authentication credentials for private registries. |
| `auth.secret_name` | | | Name of a Kubernetes secret in Connaisseur namespace that contains [dockerconfigjson](https://kubernetes.io/docs/concepts/configuration/secret/#docker-config-secrets) for registry authentication. See additional notes [below](#authentication). |
| `auth.` | | | Authentication credentials for private registries. See additional notes [below](#authentication). |
| `auth.secret_name` | | | Name of a Kubernetes secret in Connaisseur namespace that contains [dockerconfigjson](https://kubernetes.io/docs/concepts/configuration/secret/#docker-config-secrets) for registry authentication. See additional notes [below](#dockerconfigjson). |
| `auth.k8s_keychain` | false | | When true, pass `--k8s-keychain` argument to `cosign verify` in order to use workload identities for authentication. See additional notes [below](#k8s_keychain). |
| `cert` | | | A certificate in PEM format for private registries. |

### Example
Expand Down Expand Up @@ -123,8 +124,11 @@ policy:

### Authentication

When using a private registry for images and signature data, the credentials need to be provided to Connaisseur.
This is done by creating a [dockerconfigjson](https://kubernetes.io/docs/concepts/configuration/secret/#docker-config-secrets) Kubernetes secret in the Connaisseur namespace and passing the secret name to Connaisseur as `auth.secret_name`.
When using a private registry for images and signature data, the credentials need to be provided to Connaisseur. There are two ways to do this.

#### dockerconfigjson

Create a [dockerconfigjson](https://kubernetes.io/docs/concepts/configuration/secret/#docker-config-secrets) Kubernetes secret in the Connaisseur namespace and pass the secret name to Connaisseur as `auth.secret_name`.
The secret can for example be created directly from your local `config.json` (for docker this resides in `~/.docker/config.json`):

```bash
Expand All @@ -136,6 +140,30 @@ kubectl create secret generic my-secret \
In the above case, the secret name in Connaisseur configuration would be `secret_name: my-secret`.
It is possible to provide one Kubernetes secret with a `config.json` for authentication to multiple private registries and referencing this in multiple validators.

#### k8s_keychain

Specification of `auth.k8s_keychain: true` in the validator configuration passes the `--k8s-keychain` to `cosign` when performing image validation.
Thus, [k8schain](https://pkg.go.dev/github.com/google/go-containerregistry/pkg/authn/k8schain) is used by `cosign` to pick up ambient registry credentials from the environment and for example use workload identities in case of common cloud providers.

For example, when validating against an ECR private repository, the credentials of an IAM user allowed to perform actions
`ecr:GetAuthorizationToken`, `ecr:BatchGetImage`, and `ecr:GetDownloadUrlForLayer` could be added to the secret `connaisseur-env-secrets`:

```yaml
apiVersion: v1
kind: Secret
type: Opaque
metadata:
name: connaisseur-env-secrets
...
data:
AWS_ACCESS_KEY_ID: ***
AWS_SECRET_ACCESS_KEY: ***
...
```

If `k8s_keychain` is set to `true` in the validator configuration, `cosign` will log into ECR at time of validation.
See [this cosign pull request](https://github.com/sigstore/cosign/pull/972) for more details.

### KMS Support

> :warning: This is currently an experimental feature that might unstable over time. As such, it is not part of our semantic versioning guarantees and we take the liberty to adjust or remove it with any version at any time without incrementing MAJOR or MINOR.
Expand Down
23 changes: 17 additions & 6 deletions tests/validators/cosign/test_cosign_validator.py
Expand Up @@ -31,6 +31,7 @@
"key": "...",
},
],
"auth": {"k8s_keychain": True},
},
{
"name": "cosign1",
Expand All @@ -39,11 +40,13 @@
{"name": "megatest", "key": "..."},
{"name": "test", "key": "..."},
],
"auth": {"k8s_keychain": False},
},
{
"name": "cosign1",
"type": "cosign",
"trust_roots": [],
"auth": {"secret_name": "my-secret"},
},
]

Expand Down Expand Up @@ -87,11 +90,14 @@ def mock_kill(self):
pytest_subprocess.fake_popen.FakePopen.kill = mock_kill


@pytest.mark.parametrize("index", [0, 1, 2])
def test_init(index: int):
@pytest.mark.parametrize(
"index, kchain", [(0, False), (1, True), (2, False), (3, False)]
)
def test_init(index: int, kchain: bool):
val = co.CosignValidator(**static_cosigns[index])
assert val.name == static_cosigns[index]["name"]
assert val.trust_roots == static_cosigns[index]["trust_roots"]
assert val.k8s_keychain == kchain


@pytest.mark.parametrize(
Expand Down Expand Up @@ -233,7 +239,7 @@ def test_get_cosign_validated_digests(


@pytest.mark.parametrize(
"image, process_input, input_type",
"image, process_input, input_type, k8s_keychain",
[
(
"testimage:v1",
Expand All @@ -244,11 +250,13 @@ def test_get_cosign_validated_digests(
"-----END PUBLIC KEY-----\n"
),
"key",
{"secret_name": "thesecret"},
),
("testimage:v1", "k8s://connaisseur/test_key", "ref"),
("testimage:v1", "k8s://connaisseur/test_key", "ref", False),
("testimage:v1", "k8s://connaisseur/test_key", "ref", True),
],
)
def test_invoke_cosign(fake_process, image, process_input, input_type):
def test_invoke_cosign(fake_process, image, process_input, input_type, k8s_keychain):
def stdin_function(input):
return {"stderr": input.decode(), "stdout": input}

Expand All @@ -264,6 +272,7 @@ def stdin_function(input):
"text",
"--key",
"/dev/stdin" if input_type == "key" else process_input,
*(["--k8s-keychain"] if k8s_keychain else []),
image,
]
fake_process.register_subprocess(
Expand All @@ -272,7 +281,9 @@ def stdin_function(input):
stdout=bytes(cosign_payload, "utf-8"),
stdin_callable=stdin_function,
)
val = co.CosignValidator(**static_cosigns[0])
config = static_cosigns[0].copy()
config["auth"] = {"k8s_keychain": k8s_keychain}
val = co.CosignValidator(**config)
returncode, stdout, stderr = val._CosignValidator__invoke_cosign(
"testimage:v1", process_input
)
Expand Down