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

decoupling fragment feed from image push target #74

Merged
merged 1 commit into from
Nov 26, 2024
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/confcom/HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

Release History
===============
1.1.2
++++++
* fixing metadata for uploaded fragments
* fixing support for non-image feed names and attaching fragments to an image
* bug fixes for image-attached fragments
* adding ability to generate a fragment import from an image name using the remote attached fragments
* updating stdout import statement to look more like the file output

1.1.1
++++++
* updating dmverity-vhd version with bugfix for empty image layers
Expand Down
27 changes: 19 additions & 8 deletions src/confcom/azext_confcom/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -721,19 +721,30 @@ This outputs a file `fragments.json` that contains the following information:

```json
{
"path": "./contoso.rego.cose",
"feed": "contoso.azurecr.io/example",
"includes": [
"containers",
"fragments"
],
"issuer": "did:x509:0:sha256:mLzv0uyBNQvC6hi4y9qy8hr6NSZuYFv6gfCwAEWBNqc::subject:CN:Contoso",
"minimum_svn": "1"
"fragments": [
{
"feed": "contoso.azurecr.io/example",
"includes": [
"containers",
"fragments"
],
"issuer": "did:x509:0:sha256:mLzv0uyBNQvC6hi4y9qy8hr6NSZuYFv6gfCwAEWBNqc::subject:CN:Contoso",
"minimum_svn": "1"
}
]
}
```

This file is then used by `acipolicygen` to generate a policy that includes custom fragments.

Example 4: The command creates a signed policy fragment and attaches it to a specified image in an ORAS-compliant registry:

```bash
az confcom acifragmentgen --chain ./samples/certs/intermediateCA/certs/www.contoso.com.chain.cert.pem --key ./samples/certs/intermediateCA/private/ec_p384_private.pem --svn 1 --namespace contoso --input ./samples/<my-config>.json --upload-fragment --image-target contoso.azurecr.io/<my-image>:latest --feed contoso.azurecr.io/<my-feed>
```

This could be useful in scenarios where an image-attached fragment is required but the fragment's feed is different from the image's location.

## Microsoft Azure CLI 'confcom katapolicygen' Extension Examples

Run `az confcom katapolicygen --help` to see a list of supported arguments along with explanations. The following commands demonstrate the usage of different arguments to generate confidential computing security policies.
Expand Down
10 changes: 8 additions & 2 deletions src/confcom/azext_confcom/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,9 +201,15 @@
- name: Input a config file to generate a fragment with a custom namespace and debug mode enabled
text: az confcom acifragmentgen --input "./config.json" --namespace "my-namespace" --debug-mode
- name: Generate an import statement for a signed local fragment
text: az confcom acifragmentgen --fragment-path "./fragment.json" --generate-import --minimum-svn 1
text: az confcom acifragmentgen --fragment-path "./fragment.rego.cose" --generate-import --minimum-svn 1
- name: Generate a fragment and COSE sign it with a key and chain
text: az confcom acifragmentgen --image mcr.microsoft.com/azuredocs/aci-helloworld --key "./key.pem" --chain "./chain.pem" --svn 1 --namespace contoso --no-print
text: az confcom acifragmentgen --input "./config.json" --key "./key.pem" --chain "./chain.pem" --svn 1 --namespace contoso --no-print
- name: Generate a fragment import from an image name
text: az confcom acifragmentgen --image <my-image> --generate-import --minimum-svn 1
- name: Attach a fragment to a specified image
text: az confcom acifragmentgen --input "./config.json" --key "./key.pem" --chain "./chain.pem" --svn 1 --namespace contoso --upload-fragment --image-target <my-image>


"""

helps[
Expand Down
10 changes: 10 additions & 0 deletions src/confcom/azext_confcom/_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
validate_fragment_path,
validate_fragment_json,
validate_fragment_json_policy,
validate_image_target,
validate_upload_fragment,
)


Expand Down Expand Up @@ -230,6 +232,13 @@ def load_arguments(self, _):
required=False,
help="Feed for the generated policy fragment",
)
c.argument(
"image_target",
options_list=("--image-target"),
required=False,
help="Image target where the generated policy fragment is attached",
ksayid marked this conversation as resolved.
Show resolved Hide resolved
validator=validate_image_target,
)
c.argument(
"key",
options_list=("--key", "-k"),
Expand Down Expand Up @@ -301,6 +310,7 @@ def load_arguments(self, _):
options_list=("--upload-fragment", "-u"),
required=False,
help="Upload a policy fragment to a container registry",
validator=validate_upload_fragment,
)
c.argument(
"no_print",
Expand Down
18 changes: 15 additions & 3 deletions src/confcom/azext_confcom/_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,19 @@ def validate_fragment_source(namespace):
raise CLIError("Must provide either an image name or an input file to generate a fragment")


def validate_image_target(namespace):
if namespace.image_target and not namespace.upload_fragment:
raise CLIError("Must specify --upload-fragment to use --image-target")


def validate_upload_fragment(namespace):
if namespace.upload_fragment and not (namespace.key or namespace.chain):
raise CLIError("Must sign the fragment with --key and --chain to upload it")


def validate_fragment_generate_import(namespace):
if namespace.generate_import and sum(map(bool, [
namespace.fragment_path,
namespace.input_path,
namespace.image_name
])) != 1:
raise CLIError(
Expand All @@ -78,8 +87,11 @@ def validate_fragment_generate_import(namespace):
"an image name to generate an import statement"
)
)
elif namespace.generate_import and namespace.output_filename:
raise CLIError("Cannot specify an output file (--output-filename) when generating an import statement. Use --fragments-json (-j) to write to a file.")
if namespace.generate_import and namespace.output_filename:
raise CLIError(
"Cannot specify an output file (--output-filename) when generating an import statement." +
"Use --fragments-json (-j) to write to a file."
)


def validate_fragment_namespace_and_svn(namespace):
Expand Down
7 changes: 6 additions & 1 deletion src/confcom/azext_confcom/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,13 +198,18 @@
# reserved fragment names for existing pieces of Rego
RESERVED_FRAGMENT_NAMES = _config["reserved_fragment_namespaces"]
# fragment artifact type
ARTIFACT_TYPE = "application/x-ms-policy-frag"
ARTIFACT_TYPE = "application/x-ms-ccepolicy-frag"
# customer rego file for data to be injected
REGO_FILE = "./data/customer_rego_policy.txt"
REGO_FRAGMENT_FILE = "./data/customer_rego_fragment.txt"
script_directory = os.path.dirname(os.path.realpath(__file__))
REGO_FILE_PATH = f"{script_directory}/{REGO_FILE}"
REGO_FRAGMENT_FILE_PATH = f"{script_directory}/{REGO_FRAGMENT_FILE}"
REGO_IMPORT_FILE_STRUCTURE = """
{
"fragments": []
}
"""
CUSTOMER_REGO_POLICY = load_str_from_file(REGO_FILE_PATH)
CUSTOMER_REGO_FRAGMENT = load_str_from_file(REGO_FRAGMENT_FILE_PATH)
# sidecar rego file
Expand Down
3 changes: 3 additions & 0 deletions src/confcom/azext_confcom/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,9 @@ def get_id(self) -> str:
def get_name(self) -> str:
return self.containerName

def get_container_image(self) -> str:
return self.containerImage

def get_working_dir(self) -> str:
return self._workingDir

Expand Down
18 changes: 18 additions & 0 deletions src/confcom/azext_confcom/cose_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@ def cose_sign(
payload_path,
"-key",
key_path,
"-salt",
"zero",
"-content-type",
"application/unknown+rego",
"-out",
out_path,
]
Expand Down Expand Up @@ -183,3 +187,17 @@ def extract_payload_from_path(self, fragment_path: str) -> str:

stdout = item.stdout.decode("utf-8")
return stdout.split("payload:")[1]

def extract_feed_from_path(self, fragment_path: str) -> str:
policy_bin_str = str(self.policy_bin)
if not os.path.exists(fragment_path):
eprint(f"The fragment file at {fragment_path} does not exist")

arg_list_chain = [policy_bin_str, "check", "--in", fragment_path, "--verbose"]

item = call_cose_sign_tool(arg_list_chain, "Error getting information from signed fragment file")

stdout = item.stdout.decode("utf-8")

# we want the text between the name and the next newline
return stdout.split("feed: ")[1].split("\n")[0]
ksayid marked this conversation as resolved.
Show resolved Hide resolved
79 changes: 59 additions & 20 deletions src/confcom/azext_confcom/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from azext_confcom.config import (
DEFAULT_REGO_FRAGMENTS,
POLICY_FIELD_CONTAINERS_ELEMENTS_REGO_FRAGMENTS,

REGO_IMPORT_FILE_STRUCTURE,
)

from azext_confcom import os_util
Expand All @@ -22,7 +22,8 @@
inject_policy_into_template,
inject_policy_into_yaml,
print_existing_policy_from_arm_template,
print_existing_policy_from_yaml
print_existing_policy_from_yaml,
get_image_name,
)
from azext_confcom.fragment_util import get_all_fragment_contents
from azext_confcom.init_checks import run_initial_docker_checks
Expand Down Expand Up @@ -93,7 +94,6 @@ def acipolicygen_confcom(
check_infrastructure_svn(infrastructure_svn)

fragments_list = []
fragment_policy_list = []
# gather information about the fragments being used in the new policy
if include_fragments:
fragments_list = os_util.load_json_from_file(fragments_json or input_path)
Expand All @@ -102,7 +102,6 @@ def acipolicygen_confcom(
# convert to list if it's just a dict
if not isinstance(fragments_list, list):
fragments_list = [fragments_list]
fragment_policy_list = get_all_fragment_contents(fragments_list)

# telling the user what operation we're doing
logger.warning(
Expand Down Expand Up @@ -132,7 +131,6 @@ def acipolicygen_confcom(
approve_wildcards=approve_wildcards,
diff_mode=diff,
rego_imports=fragments_list,
fragment_contents=fragment_policy_list,
exclude_default_fragments=exclude_default_fragments,
)
elif image_name:
Expand All @@ -156,6 +154,21 @@ def acipolicygen_confcom(
if not isinstance(container_group_policies, list):
container_group_policies = [container_group_policies]

# get all of the fragments that are being used in the policy
# and associate them with each container group

if include_fragments:
fragment_policy_list = []
container_names = []
fragment_imports = []
for policy in container_group_policies:
fragment_imports.extend(policy.get_fragments())
for container in policy.get_images():
container_names.append(container.get_container_image())
fragment_policy_list = get_all_fragment_contents(container_names, fragment_imports)
for policy in container_group_policies:
policy.set_fragment_contents(fragment_policy_list)

for count, policy in enumerate(container_group_policies):
policy.populate_policy_content_for_all_images(
individual_image=bool(image_name), tar_mapping=tar_mapping, faster_hashing=faster_hashing
Expand Down Expand Up @@ -209,6 +222,7 @@ def acifragmentgen_confcom(
key: str,
chain: str,
minimum_svn: int,
image_target: str = "",
algo: str = "ES384",
fragment_path: str = None,
generate_import: bool = False,
Expand All @@ -224,25 +238,34 @@ def acifragmentgen_confcom(

if generate_import:
cose_client = CoseSignToolProxy()
import_statement = cose_client.generate_import_from_path(fragment_path, minimum_svn=minimum_svn)
import_statements = []
# images can have multiple fragments attached to them so we need an array to hold the import statements
if fragment_path:
import_statements = [cose_client.generate_import_from_path(fragment_path, minimum_svn=minimum_svn)]
elif image_name:
import_statements = oras_proxy.generate_imports_from_image_name(image_name, minimum_svn=minimum_svn)

fragments_file_contents = {}
fragments_list = []
if fragments_json:
logger.info("Creating/appending import statement JSON file")
if os.path.isfile(fragments_json):
logger.info("Appending import statement to JSON file")
fragments_file_contents = os_util.load_json_from_file(fragments_json)
if isinstance(fragments_file_contents, list):
logger.error("Unsupported JSON file format. Please make sure the outermost structure is not an array. An empty import file should look like: %s", REGO_IMPORT_FILE_STRUCTURE)
sys.exit(1)
fragments_list = fragments_file_contents.get(POLICY_FIELD_CONTAINERS_ELEMENTS_REGO_FRAGMENTS, [])
else:
logger.info("Creating import statement JSON file")
fragments_file_contents = {}
fragments_list = []
# convert to list if it's just a dict
if not isinstance(fragments_list, list):
fragments_list = [fragments_list]
fragments_list.append(import_statement)

fragments_file_contents[POLICY_FIELD_CONTAINERS_ELEMENTS_REGO_FRAGMENTS] = fragments_list

# convert to list if it's just a dict
if isinstance(fragments_list, dict):
fragments_list = [fragments_list]
fragments_list += import_statements

fragments_file_contents[POLICY_FIELD_CONTAINERS_ELEMENTS_REGO_FRAGMENTS] = fragments_list
if fragments_json:
os_util.write_str_to_file(fragments_json, pretty_print_func(fragments_file_contents))
else:
print(pretty_print_func(import_statement))
print(pretty_print_func(fragments_file_contents))
return

tar_mapping = tar_mapping_validation(tar_mapping_location, using_config_file=bool(input_path))
Expand All @@ -258,14 +281,30 @@ def acifragmentgen_confcom(
policy = security_policy.load_policy_from_config_file(
input_path, debug_mode=debug_mode, disable_stdio=disable_stdio
)
# get all of the fragments that are being used in the policy
# and associate them with each container group
fragment_policy_list = []
container_names = []
fragment_imports = policy.get_fragments()
ksayid marked this conversation as resolved.
Show resolved Hide resolved
for container in policy.get_images():
container_names.append(container.get_container_image())
fragment_policy_list = get_all_fragment_contents(container_names, fragment_imports)
policy.set_fragment_contents(fragment_policy_list)
policy.populate_policy_content_for_all_images(
individual_image=bool(image_name), tar_mapping=tar_mapping
)

# if no feed is provided, use the first image's feed
# to assume it's an image-attached fragment
if not image_target:
policy_images = policy.get_images()
if not policy_images:
logger.error("No images found in the policy or all images are covered by fragments")
sys.exit(1)
image_target = policy_images[0].containerImage
if not feed:
feed = policy.get_images()[0].containerImage
# strip the tag or hash off the image name so there are stable feed names
feed = get_image_name(image_target)

fragment_text = policy.generate_fragment(namespace, svn, output_type)

Expand All @@ -284,7 +323,7 @@ def acifragmentgen_confcom(

cose_proxy.cose_sign(filename, key, chain, feed, iss, algo, out_path)
if upload_fragment:
oras_proxy.attach_fragment_to_image(feed, out_path)
oras_proxy.attach_fragment_to_image(image_target, out_path)


def katapolicygen_confcom(
Expand Down
2 changes: 1 addition & 1 deletion src/confcom/azext_confcom/data/internal_config.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "1.1.1",
"version": "1.1.2-alpha.2",
"hcsshim_config": {
"maxVersion": "1.0.0",
"minVersion": "0.0.1"
Expand Down
Loading