Skip to content

Commit

Permalink
Use encrypted container images (sc2-sys#20)
Browse files Browse the repository at this point in the history
  • Loading branch information
csegarragonz authored Oct 10, 2023
1 parent c4ff04a commit d91e00d
Show file tree
Hide file tree
Showing 11 changed files with 371 additions and 114 deletions.
24 changes: 24 additions & 0 deletions apps/helloworld-knative-encrypted/service.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: helloworld-knative
annotations:
"features.knative.dev/podspec-runtimeclassname": "enabled"
spec:
template:
metadata:
labels:
apps.coco-serverless/name: helloworld-py
io.katacontainers.config.pre_attestation.enabled: "false"
spec:
runtimeClassName: kata-qemu-sev
# coco-knative: need to run user container as root
securityContext:
runAsUser: 1000
containers:
- image: csegarragonz/coco-helloworld-py:encrypted
ports:
- containerPort: 8080
env:
- name: TARGET
value: "World"
7 changes: 7 additions & 0 deletions conf-files/ocicrypt.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"key-providers": {
"attestation-agent": {
"grpc": "127.0.0.1:50000"
}
}
}
37 changes: 37 additions & 0 deletions docs/helloworld_knative_attestation.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,15 @@ following subsections (in increasing order of security):
with the right firmware, kernel, initrd, and Kata Agent configuration.
* [Signed Container Images](#signed-container-images) - Attest that the images
used have been signed with a well-known private key.
* [Encrypted Container Images](#encrypted-container-images) - In addition to
signatures, consume encrypted container images.

After that, you may jump to [running the application](#run-the-application).

> Note that, if you are running different attestation types one after the other
> you may want to clear the contents of the KBS between runs using:
> `inv kbs.clear-db`
## Firmware Digest

Before running the application, we need to generate the expected launch digest
Expand Down Expand Up @@ -109,6 +115,31 @@ Now, you may proceed to [running the application](#run-the-application).

## Encrypted Container Images

In this last section, we will configure the system to consume encrypted
container images from public registries.

To encrypt docker images, we use [`skopeo`](https://github.com/containers/skopeo).
`skopeo` can be used together with image signatures, just make sure to pass the
`--sign` flag:

```bash
inv skopeo.encrypt-container-image "docker.io/csegarragonz/coco-helloworld-py:unencrypted" --sign
```

> To check that the image is actually encrypted, you may try to run it:
> `docker run docker.io/csegarragonz/coco-helloworld-py:encrypted`
Then, as in the previous section, update the KBS to set the right signature
verification policy:

```bash
# IMPORTANT: do not use the `--clean` flag here, as the encrypt-container-image
# command will provision a secret to the KBS
inv kbs.provision-launch-digest --signature-policy verify
```

Finally, you may proceed to [running the application](#run-the-application).

## Run the application

Once the KBS has been populated with the right measurements and secrets, we can
Expand All @@ -117,3 +148,9 @@ deploy the workload just like with the `Hello world! (Knative)` app:
```bash
kubectl apply -f ./apps/helloworld-knative
```

note that if you are using encrypted images you will have to do:

```bash
kubectl apply -f ./apps/helloworld-knative-encrypted
```
2 changes: 2 additions & 0 deletions tasks/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from . import kubeadm
from . import operator
from . import sev
from . import skopeo

ns = Collection(
apps,
Expand All @@ -28,4 +29,5 @@
kubeadm,
operator,
sev,
skopeo,
)
3 changes: 1 addition & 2 deletions tasks/coco.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from invoke import task
from os.path import join
from tasks.util.env import KATA_CONFIG_DIR
from tasks.util.kbs import KBS_PORT, get_kbs_url
from tasks.util.env import KATA_CONFIG_DIR, KBS_PORT, get_kbs_url
from tasks.util.toml import read_value_from_toml, update_toml


Expand Down
140 changes: 52 additions & 88 deletions tasks/kbs.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,16 @@
from base64 import b64encode
from invoke import task
from os.path import exists, join
from os.path import exists
from subprocess import run
from tasks.util.cosign import COSIGN_PUB_KEY
from tasks.util.kbs import (
NO_SIGNATURE_POLICY,
SIMPLE_KBS_DIR,
SIMPLE_KBS_RESOURCE_PATH,
SIGNATURE_POLICY_NONE,
create_kbs_resource,
connect_to_kbs_db,
populate_signature_verification_policy,
set_launch_measurement_policy,
validate_signature_verification_policy,
)
from tasks.util.sev import get_launch_digest

SIGNATURE_POLICY_STRING_ID = "default/security-policy/test"

Expand Down Expand Up @@ -74,7 +71,7 @@ def clear_db(ctx):


@task
def provision_launch_digest(ctx, signature_policy=NO_SIGNATURE_POLICY, clean=False):
def provision_launch_digest(ctx, signature_policy=SIGNATURE_POLICY_NONE, clean=False):
"""
Provision the KBS with the launch digest for the current node
Expand All @@ -101,85 +98,52 @@ def provision_launch_digest(ctx, signature_policy=NO_SIGNATURE_POLICY, clean=Fal
if clean:
clear_db(ctx)

# First, add our launch digest to the KBS policy
ld = get_launch_digest("sev")
ld_b64 = b64encode(ld).decode()

# Create a new record
connection = connect_to_kbs_db()
with connection:
with connection.cursor() as cursor:
policy_id = 10

# When enabling signature verification, we need to provide a
# signature policy. This policy has a constant string identifier
# that the kata agent will ask for (default/security-policy/test),
# which points to a config file that specifies how to validate
# signatures
if signature_policy != NO_SIGNATURE_POLICY:
resource_path = "signature_policy_{}.json".format(signature_policy)

if signature_policy == SIGNATURE_POLICY_NONE:
# If we set a `none` signature policy, it means that we don't
# check any signatures on the pulled container images
policy_json_str = populate_signature_verification_policy(
signature_policy
)
else:
# The verify policy, checks that the image has been signed
# with a given key. As everything in the KBS, the key
# we give in the policy is an ID for another resource.
# Note that the following resource prefix is NOT required
# (i.e. we could change it to keys/cosign/1 as long as the
# corresponding resource exists)
signing_key_resource_id = "default/cosign-key/1"
policy_json_str = populate_signature_verification_policy(
signature_policy,
[
[
"docker.io/csegarragonz/coco-helloworld-py",
signing_key_resource_id,
],
[
"docker.io/csegarragonz/coco-knative-sidecar",
signing_key_resource_id,
],
],
)

# Create a resource for the signing key
signing_key_kbs_path = "cosign.pub"
signing_key_resource_path = join(
SIMPLE_KBS_RESOURCE_PATH, signing_key_kbs_path
)
sql = "INSERT INTO resources VALUES(NULL, NULL, "
sql += "'{}', '{}', {})".format(
signing_key_resource_id, signing_key_kbs_path, policy_id
)
cursor.execute(sql)

# Lastly, we copy the public signing key to the resource
# path annotated in the policy
cp_cmd = "cp {} {}".format(
COSIGN_PUB_KEY, signing_key_resource_path
)
run(cp_cmd, shell=True, check=True)

create_kbs_resource(resource_path, policy_json_str)

# Create the resource (containing the signature policy) in the KBS
sql = "INSERT INTO resources VALUES(NULL, NULL, "
sql += "'{}', '{}', {})".format(
SIGNATURE_POLICY_STRING_ID, resource_path, policy_id
)
cursor.execute(sql)

# We associate the signature policy to a digest policy, meaning
# that irrespective of what signature policy are we using (even if
# we are not checking any signatures) we will always check the FW
# digest against the measured one
sql = "INSERT INTO policy VALUES ({}, ".format(policy_id)
sql += "'[\"{}\"]', '[]', 0, 0, '[]', now(), NULL, 1)".format(ld_b64)
cursor.execute(sql)

connection.commit()
# First, we provision a launch digest policy that only allows to
# boot confidential VMs with the launch measurement that we have
# just calculated. We will associate signature verification and
# image encryption policies to this launch digest policy.
set_launch_measurement_policy()

# To make sure the launch policy is enforced, we must enable
# signature verification. This means that we also need to provide a
# signature policy. This policy has a constant string identifier
# that the kata agent will ask for (default/security-policy/test),
# which points to a config file that specifies how to validate
# signatures
resource_path = "signature_policy_{}.json".format(signature_policy)

if signature_policy == SIGNATURE_POLICY_NONE:
# If we set a `none` signature policy, it means that we don't
# check any signatures on the pulled container images (still
# necessary to set the policy to check the launch measurment)
policy_json_str = populate_signature_verification_policy(signature_policy)
else:
# The verify policy, checks that the image has been signed
# with a given key. As everything in the KBS, the key
# we give in the policy is an ID for another resource.
# Note that the following resource prefix is NOT required
# (i.e. we could change it to keys/cosign/1 as long as the
# corresponding resource exists)
signing_key_resource_id = "default/cosign-key/1"
policy_json_str = populate_signature_verification_policy(
signature_policy,
[
[
"docker.io/csegarragonz/coco-helloworld-py",
signing_key_resource_id,
],
[
"docker.io/csegarragonz/coco-knative-sidecar",
signing_key_resource_id,
],
],
)

# Create a resource for the signing key
with open(COSIGN_PUB_KEY) as fh:
create_kbs_resource(signing_key_resource_id, "cosign.pub", fh.read())

# Finally, create a resource for the image signing policy. Note that the
# resource ID for the image signing policy is hardcoded in the kata agent
# (particularly in the attestation agent)
create_kbs_resource(SIGNATURE_POLICY_STRING_ID, resource_path, policy_json_str)
103 changes: 103 additions & 0 deletions tasks/skopeo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
from base64 import b64encode
from invoke import task
from json import loads as json_loads
from os.path import exists, join
from subprocess import run
from tasks.util.cosign import sign_container_image
from tasks.util.env import CONF_FILES_DIR, K8S_CONFIG_DIR
from tasks.util.guest_components import (
start_coco_keyprovider,
stop_coco_keyprovider,
)
from tasks.util.kbs import create_kbs_secret

SKOPEO_VERSION = "1.13.0"
SKOPEO_IMAGE = "quay.io/skopeo/stable:v{}".format(SKOPEO_VERSION)
SKOPEO_ENCRYPTION_KEY = join(K8S_CONFIG_DIR, "image_enc.key")
AA_CTR_ENCRYPTION_KEY = "/tmp/image_enc.key"


def run_skopeo_cmd(cmd, capture_stdout=False):
ocicrypt_conf_host = join(CONF_FILES_DIR, "ocicrypt.conf")
ocicrypt_conf_guest = "/ocicrypt.conf"
skopeo_cmd = [
"docker run --rm",
"--net host",
"-e OCICRYPT_KEYPROVIDER_CONFIG={}".format(ocicrypt_conf_guest),
"-v {}:{}".format(ocicrypt_conf_host, ocicrypt_conf_guest),
"-v ~/.docker/config.json:/config.json",
SKOPEO_IMAGE,
cmd,
]
skopeo_cmd = " ".join(skopeo_cmd)
if capture_stdout:
return (
run(skopeo_cmd, shell=True, capture_output=True)
.stdout.decode("utf-8")
.strip()
)
else:
run(skopeo_cmd, shell=True, check=True)


def create_encryption_key():
cmd = "head -c32 < /dev/random > {}".format(SKOPEO_ENCRYPTION_KEY)
run(cmd, shell=True, check=True)


@task
def encrypt_container_image(ctx, image_tag, sign=False):
"""
Encrypt an OCI container image using Skopeo
The image tag must be provided in the format: docker.io/<repo>/<name>:tag
"""
encryption_key_resource_id = "default/image-encryption-key/1"
if not exists(SKOPEO_ENCRYPTION_KEY):
create_encryption_key()

# We use CoCo's keyprovider server (that implements the ocicrypt protocol)
# to encrypt the OCI image. To that extent, we need to mount the encryption
# key somewhere that the attestation agent (in the keyprovider) can find
# it
start_coco_keyprovider(SKOPEO_ENCRYPTION_KEY, AA_CTR_ENCRYPTION_KEY)

encrypted_image_tag = image_tag.split(":")[0] + ":encrypted"
skopeo_cmd = [
"copy --insecure-policy",
"--authfile /config.json",
"--encryption-key",
"provider:attestation-agent:keyid=kbs:///{}::keypath={}".format(
encryption_key_resource_id, AA_CTR_ENCRYPTION_KEY
),
"docker://{}".format(image_tag),
"docker://{}".format(encrypted_image_tag),
]
skopeo_cmd = " ".join(skopeo_cmd)
run_skopeo_cmd(skopeo_cmd)

# Sanity check that the image is actually encrypted
inspect_jsonstr = run_skopeo_cmd(
"inspect docker://{}".format(encrypted_image_tag), capture_stdout=True
)
inspect_json = json_loads(inspect_jsonstr)
layers = [
layer["MIMEType"].endswith("tar+gzip+encrypted")
for layer in inspect_json["LayersData"]
]
if not all(layers):
print("Some layers in image {} are not encrypted!".format(encrypted_image_tag))
stop_coco_keyprovider()
raise RuntimeError("Image encryption failed!")

# Create a secret in KBS with the encryption key. Skopeo needs it as raw
# bytes, whereas KBS wants it base64 encoded, so we do the conversion first
with open(SKOPEO_ENCRYPTION_KEY, "rb") as fh:
key_b64 = b64encode(fh.read()).decode()

create_kbs_secret(encryption_key_resource_id, key_b64)

if sign:
sign_container_image(encrypted_image_tag)

stop_coco_keyprovider()
Loading

0 comments on commit d91e00d

Please sign in to comment.