Skip to content

Commit

Permalink
Concatenate or override duplicate envvars
Browse files Browse the repository at this point in the history
With this commit, duplicate manifest keys (in particular, environment
variables) are overridden in the following order: first from a
user-provided manifest, then from the GSC-internal manifest, and then
from the manifest-shaped environment of the original Docker image.

The only exceptions are PATH, LD_LIBRARY_PATH, LD_PRELOAD envvars which
are concatenated instead of overridden. The order of concatenation is
the same as above.

Signed-off-by: jkr0103 <jitender.kumar@intel.com>
  • Loading branch information
jkr0103 authored and dimakuv committed Dec 14, 2023
1 parent 68bcee1 commit a59ff44
Show file tree
Hide file tree
Showing 2 changed files with 55 additions and 38 deletions.
32 changes: 18 additions & 14 deletions Documentation/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -265,20 +265,24 @@ follows three main stages and produces an image named ``gsc-<image-name>``.
#. **Graminizing the application image.** The second stage copies the important
Gramine artifacts (e.g., the runtime and signer tool) from the first stage
(or if the first stage was skipped, it pulls a prebuilt Docker image defined
via the configuration file). It then prepares image-specific variables such
as the executable path and the library path, and scans the entire image to
generate a list of trusted files. GSC excludes files and paths starting with
:file:`/boot`, :file:`/dev`, :file:`.dockerenv`, :file:`.dockerinit`,
:file:`/etc/mtab`, :file:`/etc/rc`, :file:`/proc`, :file:`/sys`, and
:file:`/var`, since checksums are required which either don't exist or may
vary across different deployment machines. GSC combines these variables and
list of trusted files into a new manifest file. In a last step the entrypoint
is changed to launch the :file:`apploader.sh` script which generates an Intel
SGX token (only if needed, on non-FLC platforms) and starts the
:program:`gramine-sgx` loader. Note that the generated image
(``gsc-<image-name>-unsigned``) cannot successfully load an Intel SGX
enclave, since essential files and the signature of the enclave are still
missing (see next stage).
via the configuration file). It then extracts image-specific environment
variables and scans the entire image to generate a list of trusted files. All
envvars are overridden (if duplicates found) in the following order: first
from a user-provided manifest, if not found then from the GSC-internal
manifest, and finally from the original Docker image environment. The only
exceptions are ``LD_LIBRARY_PATH``, ``PATH``, ``LD_PRELOAD``; they are
concatenated instead of overridden (concatenation order is the same as
above). GSC excludes files and paths starting with :file:`/boot`,
:file:`/dev`, :file:`.dockerenv`, :file:`.dockerinit`, :file:`/etc/mtab`,
:file:`/etc/rc`, :file:`/proc`, :file:`/sys`, and :file:`/var`, since
checksums are required which either don't exist or may vary across different
deployment machines. GSC combines these variables and list of trusted files
into a new manifest file. In a last step the entrypoint is changed to launch
the :file:`apploader.sh` script which generates an Intel SGX token (only if
needed, on non-FLC platforms) and starts the :program:`gramine-sgx` loader.
Note that the generated image (``gsc-<image-name>-unsigned``) cannot
successfully load an Intel SGX enclave, since essential files and the
signature of the enclave are still missing (see next stage).

#. **Signing the Intel SGX enclave.** The third stage uses Gramine's signer
tool to generate SIGSTRUCT files for SGX enclave initialization. This tool
Expand Down
61 changes: 37 additions & 24 deletions gsc.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,10 +127,6 @@ def extract_environment_from_image_config(config):
continue
escaped_env_var = env_var.translate(str.maketrans({'\\': r'\\', '"': r'\"'}))
env_var_name = escaped_env_var.split('=', maxsplit=1)[0]
if env_var_name in ('PATH', 'LD_LIBRARY_PATH'):
# PATH and LD_LIBRARY_PATH are already part of entrypoint.manifest.template.
# Their values are provided in finalize_manifest.py, hence skipping here.
continue
env_var_value = escaped_env_var.split('=', maxsplit=1)[1]
base_image_environment += f'loader.env.{env_var_name} = "{env_var_value}"\n'
return base_image_environment
Expand Down Expand Up @@ -167,21 +163,31 @@ def extract_user_from_image_config(config, env):
user = 'root'
env.globals.update({'app_user': user})

def merge_two_dicts(dict1, dict2, path=[]):
for key in dict2:
if key in dict1:
if isinstance(dict1[key], dict) and isinstance(dict2[key], dict):
merge_two_dicts(dict1[key], dict2[key], path + [str(key)])
elif isinstance(dict1[key], list) and isinstance(dict2[key], list):
dict1[key].extend(dict2[key])
elif dict1[key] == dict2[key]:
def merge_manifests_in_order(manifest1, manifest2, manifest1_name, manifest2_name, path=[]):
for key in manifest2:
if key in manifest1:
if isinstance(manifest1[key], dict) and isinstance(manifest2[key], dict):
merge_manifests_in_order(manifest1[key], manifest2[key], manifest1_name,
manifest2_name, path + [str(key)])
elif isinstance(manifest1[key], list) and isinstance(manifest2[key], list):
manifest1[key].extend(manifest2[key])
elif manifest1[key] == manifest2[key]:
pass
else:
raise Exception(f'''Duplicate key with different values found: `{".".join(path +
[str(key)])}`''')
# key exists in both manifests but with different values:
# - for a special case of three below envvars must concatenate the values,
# - in all other cases choose the value from the first manifest
if ('.'.join(path) == 'loader.env' and
key in ['LD_LIBRARY_PATH', 'PATH', 'LD_PRELOAD']):
manifest1[key] = f'{manifest1[key]}:{manifest2[key]}'
print(f'Warning: Duplicate key `{".".join(path + [str(key)])}`. Concatenating'
f' values from `{manifest1_name}` and `{manifest2_name}`.')
else:
print(f'Warning: Duplicate key `{".".join(path + [str(key)])}`. Overriding'
f' value from `{manifest2_name}` by the one in `{manifest1_name}`.')
else:
dict1[key] = dict2[key]
return dict1
manifest1[key] = manifest2[key]
return manifest1

def handle_redhat_repo_configs(distro, tmp_build_path):
if distro not in {"redhat/ubi8", "redhat/ubi8-minimal"}:
Expand Down Expand Up @@ -331,7 +337,8 @@ def gsc_build(args):
# - Jinja-style templates/entrypoint.manifest.template
# - base Docker image's environment variables
# - additional, user-provided manifest options
entrypoint_manifest_render = env.get_template(f'{distro}/entrypoint.manifest.template').render()
entrypoint_manifest_name = f'{distro}/entrypoint.manifest.template'
entrypoint_manifest_render = env.get_template(entrypoint_manifest_name).render()
try:
entrypoint_manifest_dict = tomli.loads(entrypoint_manifest_render)
except Exception as e:
Expand All @@ -340,23 +347,29 @@ def gsc_build(args):
sys.exit(1)

base_image_environment = extract_environment_from_image_config(original_image.attrs['Config'])
base_image_dict = tomli.loads(base_image_environment)
base_image_env_dict = tomli.loads(base_image_environment)
base_image_env_name = f'<{original_image_name} image env>'

user_manifest_name = args.manifest
user_manifest_contents = ''
if not os.path.exists(args.manifest):
print(f'Manifest file "{args.manifest}" does not exist.', file=sys.stderr)
if not os.path.exists(user_manifest_name):
print(f'Manifest file "{user_manifest_name}" does not exist.', file=sys.stderr)
sys.exit(1)
with open(args.manifest, 'r') as user_manifest_file:

with open(user_manifest_name, 'r') as user_manifest_file:
user_manifest_contents = user_manifest_file.read()

try:
user_manifest_dict = tomli.loads(user_manifest_contents)
except Exception as e:
print(f'Failed to parse the "{args.manifest}" file. Error:', e, file=sys.stderr)
print(f'Failed to parse the "{user_manifest_name}" file. Error:', e, file=sys.stderr)
sys.exit(1)

merged_manifest_dict = merge_two_dicts(user_manifest_dict, entrypoint_manifest_dict)
merged_manifest_dict = merge_two_dicts(merged_manifest_dict, base_image_dict)
merged_manifest_dict = merge_manifests_in_order(user_manifest_dict, entrypoint_manifest_dict,
user_manifest_name, entrypoint_manifest_name)
merged_manifest_name = (f'<merged {user_manifest_name} and {entrypoint_manifest_name}>')
merged_manifest_dict = merge_manifests_in_order(merged_manifest_dict, base_image_env_dict,
merged_manifest_name, base_image_env_name)

with open(tmp_build_path / 'entrypoint.manifest', 'wb') as entrypoint_manifest:
tomli_w.dump(merged_manifest_dict, entrypoint_manifest)
Expand Down

0 comments on commit a59ff44

Please sign in to comment.