diff --git a/.ansible-lint b/.ansible-lint index c6f988e..27a3522 100644 --- a/.ansible-lint +++ b/.ansible-lint @@ -21,6 +21,6 @@ exclude_paths: - .markdownlint.yaml - examples/roles/ mock_roles: - - linux-system-roles.trustee_attestation_server + - linux-system-roles.trustee_server supported_ansible_also: - "2.14.0" diff --git a/.github/workflows/tft.yml b/.github/workflows/tft.yml index f919267..22b9b68 100644 --- a/.github/workflows/tft.yml +++ b/.github/workflows/tft.yml @@ -181,7 +181,7 @@ jobs: tf_scope: private api_key: ${{ secrets.TF_API_KEY_RH }} update_pull_request_status: false - tmt_plan_filter: "tag:playbooks_parallel,trustee_attestation_server" + tmt_plan_filter: "tag:playbooks_parallel,trustee_server" - name: Set final commit status uses: myrotvorets/set-commit-status-action@master diff --git a/README.md b/README.md index 0e7803c..e3e08f4 100644 --- a/README.md +++ b/README.md @@ -1,105 +1,69 @@ -# Role Name +# trustee_server -[![ansible-lint.yml](https://github.com/linux-system-roles/trustee_attestation_server/actions/workflows/ansible-lint.yml/badge.svg)](https://github.com/linux-system-roles/trustee_attestation_server/actions/workflows/ansible-lint.yml) [![ansible-test.yml](https://github.com/linux-system-roles/trustee_attestation_server/actions/workflows/ansible-test.yml/badge.svg)](https://github.com/linux-system-roles/trustee_attestation_server/actions/workflows/ansible-test.yml) [![codespell.yml](https://github.com/linux-system-roles/trustee_attestation_server/actions/workflows/codespell.yml/badge.svg)](https://github.com/linux-system-roles/trustee_attestation_server/actions/workflows/codespell.yml) [![markdownlint.yml](https://github.com/linux-system-roles/trustee_attestation_server/actions/workflows/markdownlint.yml/badge.svg)](https://github.com/linux-system-roles/trustee_attestation_server/actions/workflows/markdownlint.yml) [![qemu-kvm-integration-tests.yml](https://github.com/linux-system-roles/trustee_attestation_server/actions/workflows/qemu-kvm-integration-tests.yml/badge.svg)](https://github.com/linux-system-roles/trustee_attestation_server/actions/workflows/qemu-kvm-integration-tests.yml) [![shellcheck.yml](https://github.com/linux-system-roles/trustee_attestation_server/actions/workflows/shellcheck.yml/badge.svg)](https://github.com/linux-system-roles/trustee_attestation_server/actions/workflows/shellcheck.yml) [![tft.yml](https://github.com/linux-system-roles/trustee_attestation_server/actions/workflows/tft.yml/badge.svg)](https://github.com/linux-system-roles/trustee_attestation_server/actions/workflows/tft.yml) [![tft_citest_bad.yml](https://github.com/linux-system-roles/trustee_attestation_server/actions/workflows/tft_citest_bad.yml/badge.svg)](https://github.com/linux-system-roles/trustee_attestation_server/actions/workflows/tft_citest_bad.yml) [![woke.yml](https://github.com/linux-system-roles/trustee_attestation_server/actions/workflows/woke.yml/badge.svg)](https://github.com/linux-system-roles/trustee_attestation_server/actions/workflows/woke.yml) +[![ansible-lint.yml](https://github.com/linux-system-roles/trustee_server/actions/workflows/ansible-lint.yml/badge.svg)](https://github.com/linux-system-roles/trustee_server/actions/workflows/ansible-lint.yml) [![ansible-test.yml](https://github.com/linux-system-roles/trustee_server/actions/workflows/ansible-test.yml/badge.svg)](https://github.com/linux-system-roles/trustee_server/actions/workflows/ansible-test.yml) [![codespell.yml](https://github.com/linux-system-roles/trustee_server/actions/workflows/codespell.yml/badge.svg)](https://github.com/linux-system-roles/trustee_server/actions/workflows/codespell.yml) [![markdownlint.yml](https://github.com/linux-system-roles/trustee_server/actions/workflows/markdownlint.yml/badge.svg)](https://github.com/linux-system-roles/trustee_server/actions/workflows/markdownlint.yml) [![qemu-kvm-integration-tests.yml](https://github.com/linux-system-roles/trustee_server/actions/workflows/qemu-kvm-integration-tests.yml/badge.svg)](https://github.com/linux-system-roles/trustee_server/actions/workflows/qemu-kvm-integration-tests.yml) [![shellcheck.yml](https://github.com/linux-system-roles/trustee_server/actions/workflows/shellcheck.yml/badge.svg)](https://github.com/linux-system-roles/trustee_server/actions/workflows/shellcheck.yml) [![tft.yml](https://github.com/linux-system-roles/trustee_server/actions/workflows/tft.yml/badge.svg)](https://github.com/linux-system-roles/trustee_server/actions/workflows/tft.yml) [![tft_citest_bad.yml](https://github.com/linux-system-roles/trustee_server/actions/workflows/tft_citest_bad.yml/badge.svg)](https://github.com/linux-system-roles/trustee_server/actions/workflows/tft_citest_bad.yml) [![woke.yml](https://github.com/linux-system-roles/trustee_server/actions/workflows/woke.yml/badge.svg)](https://github.com/linux-system-roles/trustee_server/actions/workflows/woke.yml) -![template](https://github.com/linux-system-roles/template/workflows/tox/badge.svg) +![trustee_server](https://github.com/linux-system-roles/trustee_server/workflows/tox/badge.svg) -A template for an ansible role that configures some GNU/Linux subsystem or -service. A brief description of the role goes here. +An Ansible role that deploys [Trustee](https://confidentialcontainers.org/docs/attestation/) server components for confidential computing. Trustee provides attestation and secret delivery services (KBS, AS, RVPS) for workloads running in Trusted Execution Environments (TEEs). -## Requirements +## Features + +- **Trustee Server (Quadlet)**: Deploys Trustee Key Broker Service(KBS), Attestation Service(AS) and Reference Value Provider Service(RVPS) using Podman Quadlets from a GitHub repository +- **Secret Registration Server**: HTTPS service that receives attestation-backed registration requests, verifies attestation, creates disk encryption keys, and stores them in Trustee KBS -Any prerequisites that may not be covered by Ansible itself or the role should -be mentioned here. This includes platform dependencies not managed by the -role, hardware requirements, external collections, etc. There should be a -distinction between *control node* requirements (like collections) and -*managed node* requirements (like special hardware, platform provisioning). +## Requirements -### Collection requirements +### Control node -For instance, if the role depends on some collections and has a -`meta/collection-requirements.yml` file for installing those dependencies, and -in order to manage `rpm-ostree` systems, it should be mentioned here that the - user should run +- Ansible 2.9 or later +- Install collection dependencies: ```bash -ansible-galaxy collection install -vv -r meta/collection-requirements.yml +ansible-galaxy collection install -r meta/collection-requirements.yml ``` -on the *control node* before using the role. - -## Role Variables - -A description of all input variables (i.e. variables that are defined in -`defaults/main.yml`) for the role should go here as these form an API of the -role. Each variable should have its own section e.g. - -### template_foo - -This variable is required. It is a string that lists the foo of the role. -There is no default value. - -### template_bar - -This variable is optional. It is a boolean that tells the role to disable bar. -The default value is `true`. - -Variables that are not intended as input, like variables defined in -`vars/main.yml`, variables that are read from other roles and/or the global -scope (ie. hostvars, group vars, etc.) can be also mentioned here but keep in -mind that as these are probably not part of the role API they may change during -the lifetime. - -Example of setting the variables: +## Example Playbook ```yaml -template_foo: "oof" -template_bar: false +- name: Deploy Trustee Server + hosts: all + vars: + trustee_server_trustee: true + trustee_server_secret_registration_enabled: true + trustee_server_secret_registration_listen_port: 8081 + roles: + - linux-system-roles.trustee_server ``` -## Variables Exported by the Role +More examples are in the [`examples/`](examples) directory. -This section is optional. Some roles may export variables for playbooks to -use later. These are analogous to "return values" in Ansible modules. For -example, if a role performs some action that will require a system reboot, but -the user wants to defer the reboot, the role might set a variable like -`template_reboot_needed: true` that the playbook can use to reboot at a more -convenient time. +## Trustee Server -Example: +When enabled, the role: -### template_reboot_needed - -Default `false` - if `true`, this means a reboot is needed to apply the changes -made by the role - -## Example Playbook +1. Downloads the Podman Quadlets from designated repo +2. Generates all required certificates of Trustee server components +3. Add KBS port 8080 to firewalld +4. Enables the services by default -Including an example of how to use your role (for instance, with variables -passed in as parameters) is always nice for users too: +Note that KBS listens on port 8080 which may require additional network security allowance depending on your environment. -```yaml -- name: Manage the template subsystem - hosts: all - vars: - template_foo: "foo foo!" - template_bar: false - roles: - - linux-system-roles.template -``` +## Secret Registration Server -More examples can be provided in the [`examples/`](examples) directory. These -can be useful, especially for documentation. +When enabled, the secret registration server: -## rpm-ostree +1. Listens for `POST /register-encryption-key` with `attestation_token` and `client_id` (machine-id) +2. Verifies the attestation token (Azure TPM-based) +3. Creates a disk encryption key and stores it in Trustee KBS +4. Appends resource policy to `/etc/trustee/kbs/policy.rego` -See README-ostree.md +Clients can then fetch the key from Trustee CDH using attestation. ## License -Whenever possible, please prefer MIT. +MIT -## Author Information +## Author An optional section for the role authors to include contact information, or a website (HTML is not allowed). diff --git a/contributing.md b/contributing.md index 3c4d34d..e05d6a6 100644 --- a/contributing.md +++ b/contributing.md @@ -1,4 +1,4 @@ -# Contributing to the trustee_attestation_server Linux System Role +# Contributing to the trustee_server Linux System Role ## Where to start @@ -12,12 +12,12 @@ This has all of the common information that all role developers need: * How to create git commits and submit pull requests **Bugs and needed implementations** are listed on -[Github Issues](https://github.com/linux-system-roles/trustee_attestation_server/issues). +[Github Issues](https://github.com/linux-system-roles/trustee_server/issues). Issues labeled with -[**help wanted**](https://github.com/linux-system-roles/trustee_attestation_server/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22) +[**help wanted**](https://github.com/linux-system-roles/trustee_server/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22) are likely to be suitable for new contributors! -**Code** is managed on [Github](https://github.com/linux-system-roles/trustee_attestation_server), using +**Code** is managed on [Github](https://github.com/linux-system-roles/trustee_server), using [Pull Requests](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests). ## Running CI Tests Locally diff --git a/defaults/main.yml b/defaults/main.yml index 6944529..b755ab5 100644 --- a/defaults/main.yml +++ b/defaults/main.yml @@ -3,6 +3,7 @@ # Here is the right place to put the role's input variables. # This file also serves as a documentation for such a variables. -# Examples of role input variables: -template_foo: foo -template_bar: true +trustee_server_trustee: true +# Secret registration server service configuration +trustee_server_secret_registration_enabled: false +trustee_server_secret_registration_listen_port: 8081 diff --git a/examples/simple.yml b/examples/simple.yml index 4f1456e..56c1100 100644 --- a/examples/simple.yml +++ b/examples/simple.yml @@ -1,9 +1,9 @@ # SPDX-License-Identifier: MIT --- -- name: Example template role invocation +- name: Deploy Trustee Server Components using Podman Quadlets from GitHub repository hosts: all vars: - template_foo: example variable value - template_bar: false + trustee_server_trustee: true + trustee_server_secret_registration_enabled: false roles: - - linux-system-roles.template + - linux-system-roles.trustee_server diff --git a/handlers/main.yml b/handlers/main.yml index 726022e..41893e2 100644 --- a/handlers/main.yml +++ b/handlers/main.yml @@ -1,7 +1,14 @@ # SPDX-License-Identifier: MIT --- -- name: Handler for template to restart services - service: +- name: Reload systemd daemon for trustee + ansible.builtin.systemd: + daemon_reload: true + listen: "restart trustee services" + +- name: Enable and restart trustee services + ansible.builtin.systemd: name: "{{ item }}" + enabled: true state: restarted - loop: "{{ __template_services }}" + loop: "{{ __trustee_server_services | default([]) }}" + listen: "restart trustee services" diff --git a/meta/main.yml b/meta/main.yml index c80d1e0..e0f4056 100644 --- a/meta/main.yml +++ b/meta/main.yml @@ -1,9 +1,9 @@ # SPDX-License-Identifier: MIT --- galaxy_info: - author: John Doe - description: Basic template for Linux system roles - company: John Doe, Inc. + author: Li Tian + description: Deploy Trustee Server Components using Podman Quadlets from GitHub repository + company: Red Hat, Inc. license: MIT min_ansible_version: "2.9" platforms: @@ -14,13 +14,15 @@ galaxy_info: versions: - "9" galaxy_tags: + - trustee + - attestation - el9 - el10 - fedora # Support running this role in system container environments, and enable # tests. Remove if not applicable. - - container + # - container # Support running this role during container builds (mostly useful for # bootc), and enable tests. Remove if not applicable. - - containerbuild + # - containerbuild dependencies: [] diff --git a/plans/README-plans.md b/plans/README-plans.md index d6d732a..58cd1b4 100644 --- a/plans/README-plans.md +++ b/plans/README-plans.md @@ -1,6 +1,6 @@ # Introduction CI Testing Plans -Linux System Roles CI runs [tmt](https://tmt.readthedocs.io/en/stable/index.html) test plans in [Testing farm](https://docs.testing-farm.io/Testing%20Farm/0.1/index.html) with the [tft.yml](https://github.com/linux-system-roles/trustee_attestation_server/blob/main/.github/workflows/tft.yml) GitHub workflow. +Linux System Roles CI runs [tmt](https://tmt.readthedocs.io/en/stable/index.html) test plans in [Testing farm](https://docs.testing-farm.io/Testing%20Farm/0.1/index.html) with the [tft.yml](https://github.com/linux-system-roles/trustee_server/blob/main/.github/workflows/tft.yml) GitHub workflow. The `plans/test_playbooks_parallel.fmf` plan is a test plan that runs test playbooks in parallel on multiple managed nodes. `plans/test_playbooks_parallel.fmf` is generated centrally from `https://github.com/linux-system-roles/.github/`. @@ -16,7 +16,7 @@ The `plans/test_playbooks_parallel.fmf` plan does the following steps: 2. Does the required preparation on systems. 3. For the given role and the given PR, runs the general test from [test.sh](https://github.com/linux-system-roles/tft-tests/blob/main/tests/general/test.sh). -The [tft.yml](https://github.com/linux-system-roles/trustee_attestation_server/blob/main/.github/workflows/tft.yml) workflow runs the above plan and uploads the results to our Fedora storage for public access. +The [tft.yml](https://github.com/linux-system-roles/trustee_server/blob/main/.github/workflows/tft.yml) workflow runs the above plan and uploads the results to our Fedora storage for public access. This workflow uses Testing Farm's Github Action [Schedule tests on Testing Farm](https://github.com/marketplace/actions/schedule-tests-on-testing-farm). ## Running Tests @@ -47,7 +47,7 @@ You can run tests locally with the `tmt try` cli or remotely in Testing Farm. $ TESTING_FARM_API_TOKEN= \ testing-farm request --pipeline-type="tmt-multihost" \ --plan-filter="tag:playbooks_parallel" \ - --git-url "https://github.com//trustee_attestation_server" \ + --git-url "https://github.com//trustee_server" \ --git-ref "" \ --compose CentOS-Stream-9 \ -e "SYSTEM_ROLES_ONLY_TESTS=tests_default.yml" \ diff --git a/plans/test_playbooks_parallel.fmf b/plans/test_playbooks_parallel.fmf index 727299c..6f223d4 100644 --- a/plans/test_playbooks_parallel.fmf +++ b/plans/test_playbooks_parallel.fmf @@ -10,7 +10,7 @@ provision: environment: # ensure versions are strings! SR_ANSIBLE_VER: "2.17" - SR_REPO_NAME: trustee_attestation_server + SR_REPO_NAME: trustee_server SR_PYTHON_VERSION: "3.12" SR_ONLY_TESTS: "" # tests_default.yml SR_TEST_LOCAL_CHANGES: true diff --git a/tasks/main.yml b/tasks/main.yml index 6eb72a5..ad87fb1 100644 --- a/tasks/main.yml +++ b/tasks/main.yml @@ -4,24 +4,12 @@ include_tasks: tasks/set_vars.yml # Examples of some tasks: -- name: Ensure required packages are installed - package: - name: "{{ __template_packages }}" - state: present - use: "{{ (__template_is_ostree | d(false)) | - ternary('ansible.posix.rhel_rpm_ostree', omit) }}" +- name: Deploy Trustee Server Components using Podman Quadlets + include_tasks: trustee_quadlet.yml + when: trustee_server_trustee | bool -- name: Ensure required services are enabled and started - service: - name: "{{ item }}" - state: started - enabled: true - loop: "{{ __template_services }}" - -- name: Generate /etc/{{ __template_foo_config }} - template: - src: "{{ __template_foo_config }}.j2" - dest: /etc/{{ __template_foo_config }} - backup: true - mode: "0400" - notify: Handler for template to restart services +- name: Deploy Secret Registration Server Service + include_tasks: secret_registration_server.yml + when: + - trustee_server_secret_registration_enabled | bool + - trustee_server_trustee | bool diff --git a/tasks/secret_registration_server.yml b/tasks/secret_registration_server.yml new file mode 100644 index 0000000..4465d0e --- /dev/null +++ b/tasks/secret_registration_server.yml @@ -0,0 +1,44 @@ +# SPDX-License-Identifier: MIT +--- +# Secret registration server: receives client requests with Trustee attestation, +# and client ID (machine-id), creates disk encryption keys and stores them in KBS. +# Requires Trustee (trustee_quadlet) to be deployed. + +- name: Ensure secret registration server dependencies are installed + ansible.builtin.package: + name: "{{ __trustee_server_secret_registration_packages }}" + state: present + use: "{{ (__trustee_server_is_ostree | d(false)) | + ternary('ansible.posix.rhel_rpm_ostree', omit) }}" + +- name: Deploy secret registration server script + ansible.builtin.template: + src: secret_registration_server.py.j2 + dest: "/usr/local/bin/secret_registration_server.py" + mode: "0755" + register: __trustee_server_secret_reg_script + +- name: Deploy secret registration server systemd unit + ansible.builtin.template: + src: secret_registration_server.service.j2 + dest: /etc/systemd/system/secret_registration_server.service + mode: "0644" + register: __trustee_server_secret_reg_service + +- name: Gather service facts for firewall check + ansible.builtin.service_facts: + +- name: Allow secret registration server port in firewall + ansible.posix.firewalld: + port: "{{ trustee_server_secret_registration_listen_port }}/tcp" + permanent: true + immediate: true + state: enabled + when: (ansible_facts.services | default({})).get('firewalld.service', {}).get('state', '') == 'running' + +- name: Append secret registration server service to the list of services to restart + set_fact: + __trustee_server_services: >- + {{ __trustee_server_services | default([]) + ['secret_registration_server'] }} + changed_when: true + notify: "restart trustee services" diff --git a/tasks/set_vars.yml b/tasks/set_vars.yml index c1ef3f6..641c518 100644 --- a/tasks/set_vars.yml +++ b/tasks/set_vars.yml @@ -1,12 +1,12 @@ --- - name: Ensure ansible_facts used by role setup: - gather_subset: "{{ __template_required_facts_subsets }}" - when: __template_required_facts | + gather_subset: "{{ __trustee_server_required_facts_subsets }}" + when: __trustee_server_required_facts | difference(ansible_facts.keys() | list) | length > 0 - name: Determine if system is ostree and set flag - when: not __template_is_ostree is defined + when: not __trustee_server_is_ostree is defined block: - name: Check if system is ostree stat: @@ -15,7 +15,7 @@ - name: Set flag to indicate system is ostree set_fact: - __template_is_ostree: "{{ __ostree_booted_stat.stat.exists }}" + __trustee_server_is_ostree: "{{ __ostree_booted_stat.stat.exists }}" - name: Set platform/version specific variables include_vars: "{{ __vars_file }}" diff --git a/tasks/trustee_quadlet.yml b/tasks/trustee_quadlet.yml new file mode 100644 index 0000000..f100507 --- /dev/null +++ b/tasks/trustee_quadlet.yml @@ -0,0 +1,130 @@ +# SPDX-License-Identifier: MIT +--- +- name: Ensure required packages are installed + ansible.builtin.package: + name: "{{ __trustee_server_trustee_packages }}" + state: present + use: "{{ (__trustee_server_is_ostree | d(false)) | + ternary('ansible.posix.rhel_rpm_ostree', omit) }}" + +- name: Ensure quadlet install directory exists + ansible.builtin.file: + path: "{{ __trustee_server_quadlet_install_dir }}" + state: directory + mode: "0755" + +- name: Create a temporary directory for the quadlet repository + ansible.builtin.tempfile: + state: directory + register: __trustee_server_quadlet_repo_dir + +- name: Download Trustee Server quadlet files from GitHub repository + ansible.builtin.git: + repo: "{{ __trustee_server_quadlet_repo_url }}" + dest: "{{ __trustee_server_quadlet_repo_dir.path }}" + version: "{{ __trustee_server_quadlet_repo_branch }}" + depth: 1 + force: true + register: quadlet_repo_download + +- name: Find Trustee Server quadlet files in repository + ansible.builtin.find: + paths: "{{ __trustee_server_quadlet_repo_dir.path }}/{{ __trustee_server_quadlet_repo_path }}" + patterns: + - "*.container" + - "*.volume" + - "*.network" + - "*.pod" + recurse: false + register: quadlet_files_found + +- name: Fail if no Trustee Server quadlet files found + ansible.builtin.fail: + msg: "No quadlet files found in {{ __trustee_server_quadlet_repo_url }}/{{ __trustee_server_quadlet_repo_path }}" + when: quadlet_files_found.files | length == 0 + +- name: Copy Trustee Server quadlet files to install directory + ansible.builtin.copy: + src: "{{ item.path }}" + dest: "{{ __trustee_server_quadlet_install_dir }}/{{ item.path | basename }}" + mode: "0644" + remote_src: true + force: true + loop: "{{ quadlet_files_found.files }}" + register: quadlet_files_copied + +- name: Stat repository configs directory + ansible.builtin.stat: + path: "{{ __trustee_server_quadlet_repo_dir.path }}/configs" + register: __repo_configs_dir + +- name: Copy Trustee Server config files to config directory + ansible.builtin.copy: + src: "{{ __trustee_server_quadlet_repo_dir.path }}/configs/" + dest: "{{ __trustee_server_config_dir }}/" + mode: "0644" + remote_src: true + force: true + when: __repo_configs_dir.stat.exists + +- name: Generate certificates for all components + ansible.builtin.shell: | + # Trustee Server SSL + if [ ! -f {{ __trustee_server_config_dir }}/kbs/server.key ] || [ ! -f {{ __trustee_server_config_dir }}/kbs/server.crt ]; then + openssl req -x509 -newkey rsa:2048 -nodes -keyout {{ __trustee_server_config_dir }}/kbs/server.key \ + -subj "/CN=$(hostname -f)/O=Red Hat" \ + -addext "basicConstraints=CA:FALSE" \ + -addext "keyUsage=digitalSignature,keyEncipherment" \ + -addext "extendedKeyUsage=serverAuth" \ + -addext "subjectAltName=DNS:$(hostname -f)" \ + -out {{ __trustee_server_config_dir }}/kbs/server.crt + fi + # KBS authentication key pair + if [ ! -f {{ __trustee_server_config_dir }}/kbs/auth.key ] || [ ! -f {{ __trustee_server_config_dir }}/kbs/auth.pub ]; then + openssl genpkey -algorithm ed25519 -out {{ __trustee_server_config_dir }}/kbs/auth.key + openssl pkey -in {{ __trustee_server_config_dir }}/kbs/auth.key -pubout -out {{ __trustee_server_config_dir }}/kbs/auth.pub + fi + # Attestation Service token signer key pair + if [ ! -f {{ __trustee_server_config_dir }}/as/token.key ] || [ ! -f {{ __trustee_server_config_dir }}/as/token.crt ]; then + openssl ecparam -name prime256v1 -genkey -noout -out {{ __trustee_server_config_dir }}/as/token.key + openssl req -new -x509 -key {{ __trustee_server_config_dir }}/as/token.key \ + -out {{ __trustee_server_config_dir }}/as/token.crt -days 3550 \ + -subj "/CN=as-token-signer/O=Red Hat" + mkdir -p {{ __trustee_server_config_dir }}/kbs/trusted_certs + cp {{ __trustee_server_config_dir }}/as/token.crt {{ __trustee_server_config_dir }}/kbs/trusted_certs/token0.crt + fi + changed_when: true + no_log: true + +- name: Gather service facts + ansible.builtin.service_facts: + +- name: Allow port 8080 in firewall + ansible.posix.firewalld: + port: "8080/tcp" + permanent: true + immediate: true + state: enabled + when: (ansible_facts.services | default({})).get('firewalld.service', {}).get('state', '') == 'running' + +- name: Get the installed Trustee Server pod name + ansible.builtin.find: + paths: "{{ __trustee_server_quadlet_install_dir }}" + patterns: "*.pod" + register: __trustee_server_pod_name + +- name: Enable and start Trustee Server services + ansible.builtin.systemd: + name: "{{ __trustee_server_pod_name.files[0].path | basename | replace('.pod', '-pod.service') }}" + enabled: true + state: restarted + daemon_reload: true + when: __trustee_server_pod_name.files | length > 0 + failed_when: false + +# TODO keep the server.crt and DNS names in the role variables + +- name: Clean up temporary repository directory + ansible.builtin.file: + path: "{{ __trustee_server_quadlet_repo_dir.path }}" + state: absent diff --git a/templates/foo.conf.j2 b/templates/foo.conf.j2 deleted file mode 100644 index 5fc204b..0000000 --- a/templates/foo.conf.j2 +++ /dev/null @@ -1,9 +0,0 @@ -# SPDX-License-Identifier: MIT -# -# Example of a template of configuration file -# -{{ ansible_managed | comment }} -{{ "system_role:template" | comment(prefix="", postfix="") }} -[foo] -foo = {{ template_foo }} -bar = {{ template_bar }} diff --git a/templates/secret_registration_server.py.j2 b/templates/secret_registration_server.py.j2 new file mode 100644 index 0000000..26b1f74 --- /dev/null +++ b/templates/secret_registration_server.py.j2 @@ -0,0 +1,188 @@ +{{ '#!/usr/bin/env python3' }} +# shebang template to avoid sanity test error +# SPDX-License-Identifier: MIT +{{ ansible_managed | comment }} +{{ "system_role:trustee_server" | comment(prefix="", postfix="") }} + +""" +Secret Registration Server - receives attestation-backed registration requests, +verifies attestation with Trustee, creates disk encryption keys and stores them in KBS. +""" +import json +import logging +import os +import secrets +import socket +import ssl +import subprocess +import tempfile +import base64 +from http.server import HTTPServer, BaseHTTPRequestHandler +from urllib.parse import urlparse + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s %(levelname)s: %(message)s", +) +LOG = logging.getLogger(__name__) + +# Configuration from environment or defaults +LISTEN_ADDRESS = "0.0.0.0" +LISTEN_PORT = {{ trustee_server_secret_registration_listen_port }} +KBS_AUTH_KEY = "/etc/trustee/kbs/auth.key" +POLICY_FILE = "/etc/trustee/kbs/policy.rego" +SSL_CERT = "/etc/trustee/kbs/server.crt" +SSL_KEY = "/etc/trustee/kbs/server.key" + + +def get_evidence_from_attestation_token(token: str) -> dict: + """Get PCR15 from attestation token.""" + payload = json.loads(base64.b64decode(token.split('.')[1] + "==").decode('utf-8')) + evidence = payload["submods"]["cpu0"]["ear.veraison.annotated-evidence"] + # Only support Azure TPM-based attestation for now + for tee in ["azsnpvtpm", "aztdxvtpm"]: + if evidence[tee]["tpm"]["pcr15"] is not None: + return evidence + return None + + +def store_key_in_kbs(resource_path: str, key_data: bytes) -> bool: + """Store key in Trustee KBS using kbs-client container or filesystem backend.""" + tmp_path = None + try: + with tempfile.NamedTemporaryFile(mode="wb", delete=False, suffix=".key") as f: + f.write(key_data) + tmp_path = f.name + # Trustee quadlet places version.env under /etc/trustee/version.env + kbs_client_image = os.getenv("IMAGE_SOURCE", "") + "/trustee-kbs-client:" + os.getenv("TRUSTEE_VERSION", "") + hostname = subprocess.check_output(["hostname", "-f"]).decode("utf-8").strip() + result = subprocess.run( + [ + "podman", "run", "--rm", "--network", "host", + "-v", f"{KBS_AUTH_KEY}:/auth.key:ro,Z", + "-v", f"{tmp_path}:/resource:ro,Z", + "-v", f"{SSL_CERT}:/server.crt:ro,Z", + kbs_client_image, + "kbs-client", + "--url", f"https://{hostname}:8080", + "--cert-file", "/server.crt", + "config", + "--auth-private-key", "/auth.key", + "set-resource", + "--path", resource_path, + "--resource-file", "/resource" + ], + capture_output=True, + text=True, + timeout=60, + ) + if result.returncode != 0: + LOG.error("kbs-client failed: %s", result.stderr or result.stdout) + return False + return True + except Exception as e: + LOG.exception("Error storing key in KBS: %s", e) + return False + finally: + if tmp_path and os.path.exists(tmp_path): + os.unlink(tmp_path) + + +def append_resource_policy(resource_path: str, evidence: dict, client_id: str) -> bool: + """Append key policy to resource policy file.""" + try: + with open(POLICY_FILE, "a+") as f: + f.seek(0) + if f"# machine-id: {client_id}" in f.read(): + LOG.exception("Resource policy already exists for machine-id: %s", client_id) + return False + tee_key = list(evidence.keys())[0] + pcr15_val = evidence[tee_key]["tpm"]["pcr15"] + {% raw %}policy = f'''# machine-id: {client_id} +allow {{ + #input["submods"]["cpu0"]["ear.status"] == "affirming" + path[0] == "resource" + path[1] == "disk-encryption" + path[2] == "{client_id}" + input_tcb["{tee_key}"]["tpm"]["pcr15"] == "{pcr15_val}" +}} +''' +{% endraw %} + f.write(policy + "\n") + return True + except Exception as e: + LOG.exception("Error appending resource policy: %s", e) + return False + + +class SecretRegistrationHandler(BaseHTTPRequestHandler): + """HTTP request handler for secret registration.""" + + def _send_json(self, status: int, data: dict): + body = json.dumps(data).encode() + self.send_response(status) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + self.wfile.flush() + + def do_POST(self): + if urlparse(self.path).path != "/register-encryption-key": + self.send_error(405, "Method Not Allowed") + return + content_length = int(self.headers.get("Content-Length", 0)) + try: + body = self.rfile.read(content_length) + data = json.loads(body) + except (json.JSONDecodeError, ValueError) as e: + LOG.warning("Invalid request body: %s", e) + self._send_json(400, {"error": "Invalid JSON"}) + return + + # Parse request body + attestation_token = data.get("attestation_token") + if not attestation_token: + self._send_json(400, {"error": "Missing attestation_token"}) + return + client_id = "".join(c for c in data.get("client_id", "unknown") if c.isalnum() or c in "-_") or "unknown" + evidence = get_evidence_from_attestation_token(attestation_token) + if not evidence: + self._send_json(400, {"error": "No Azure TPM-based attestation found in attestation token"}) + return + + # Create disk encryption key (32 bytes for LUKS2) + key = secrets.token_bytes(32) + resource_path = f"disk-encryption/{client_id}/luks-key-0" + if not store_key_in_kbs(resource_path, key): + self._send_json(500, {"error": "Failed to store key in KBS"}) + return + + # Append resource policy + if not append_resource_policy(resource_path, evidence, client_id): + self._send_json(500, {"error": "Failed to append resource policy"}) + return + + LOG.info("Registered disk encryption key for client %s at %s", client_id, resource_path) + self._send_json(200, { + "resource_path": resource_path, + "message": "Disk encryption key registered successfully", + }) + + +def main(): + server = HTTPServer((LISTEN_ADDRESS, LISTEN_PORT), SecretRegistrationHandler) + context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + context.load_cert_chain(certfile=SSL_CERT, keyfile=SSL_KEY) + server.socket = context.wrap_socket(server.socket, server_side=True) + LOG.info("Secret registration server listening on https://%s:%d", LISTEN_ADDRESS, LISTEN_PORT) + try: + server.serve_forever() + except KeyboardInterrupt: + pass + finally: + server.server_close() + + +if __name__ == "__main__": + main() diff --git a/templates/secret_registration_server.service.j2 b/templates/secret_registration_server.service.j2 new file mode 100644 index 0000000..07b726c --- /dev/null +++ b/templates/secret_registration_server.service.j2 @@ -0,0 +1,29 @@ +# SPDX-License-Identifier: MIT +{{ ansible_managed | comment }} +{{ "system_role:trustee_server" | comment(prefix="", postfix="") }} + +[Unit] +Description=Secret Registration Server - attestation-backed disk key registration +Documentation=https://confidentialcontainers.org/docs/attestation/ +After=network-online.target +Wants=network-online.target +# Start after Trustee pod if it exists +After=trustee-pod.service + +[Service] +Type=simple +ExecStart=/usr/bin/python3 /usr/local/bin/secret_registration_server.py +Restart=on-failure +RestartSec=5s +# Required for KBS and attestation access +Environment=PYTHONUNBUFFERED=1 +EnvironmentFile=-/etc/trustee/version.env + +# Security hardening +NoNewPrivileges=yes +PrivateTmp=yes +ProtectHome=yes +ReadWritePaths=/tmp + +[Install] +WantedBy=multi-user.target diff --git a/tests/roles/linux-system-roles.template/defaults b/tests/roles/linux-system-roles.trustee_server/defaults similarity index 100% rename from tests/roles/linux-system-roles.template/defaults rename to tests/roles/linux-system-roles.trustee_server/defaults diff --git a/tests/roles/linux-system-roles.template/handlers b/tests/roles/linux-system-roles.trustee_server/handlers similarity index 100% rename from tests/roles/linux-system-roles.template/handlers rename to tests/roles/linux-system-roles.trustee_server/handlers diff --git a/tests/roles/linux-system-roles.template/meta b/tests/roles/linux-system-roles.trustee_server/meta similarity index 100% rename from tests/roles/linux-system-roles.template/meta rename to tests/roles/linux-system-roles.trustee_server/meta diff --git a/tests/roles/linux-system-roles.template/tasks b/tests/roles/linux-system-roles.trustee_server/tasks similarity index 100% rename from tests/roles/linux-system-roles.template/tasks rename to tests/roles/linux-system-roles.trustee_server/tasks diff --git a/tests/roles/linux-system-roles.template/templates b/tests/roles/linux-system-roles.trustee_server/templates similarity index 100% rename from tests/roles/linux-system-roles.template/templates rename to tests/roles/linux-system-roles.trustee_server/templates diff --git a/tests/roles/linux-system-roles.template/vars b/tests/roles/linux-system-roles.trustee_server/vars similarity index 100% rename from tests/roles/linux-system-roles.template/vars rename to tests/roles/linux-system-roles.trustee_server/vars diff --git a/tests/setup-snapshot.yml b/tests/setup-snapshot.yml index a7704df..a30e2c1 100644 --- a/tests/setup-snapshot.yml +++ b/tests/setup-snapshot.yml @@ -4,11 +4,11 @@ tasks: - name: Set platform/version specific variables include_role: - name: linux-system-roles.template + name: linux-system-roles.trustee_server tasks_from: set_vars.yml public: true - name: Install test packages package: - name: "{{ __template_packages }}" + name: "{{ __trustee_server_trustee_packages }}" state: present diff --git a/tests/tests_default.yml b/tests/tests_default.yml index 4457422..73adb66 100644 --- a/tests/tests_default.yml +++ b/tests/tests_default.yml @@ -2,12 +2,76 @@ --- - name: Ensure that the role runs with default parameters hosts: all - gather_facts: false # test that role works in this case - roles: - - linux-system-roles.template + gather_facts: true tasks: - - name: Check header for ansible_managed, fingerprint - include_tasks: tasks/check_header.yml - vars: - __file: /etc/foo.conf - __fingerprint: system_role:template + - name: Include trustee_server role + ansible.builtin.include_role: + name: linux-system-roles.trustee_server + public: true + + - name: Gather package facts + ansible.builtin.package_facts: + + - name: Assert required packages are installed + ansible.builtin.assert: + that: item in ansible_facts.packages + fail_msg: "Required package {{ item }} is not installed" + loop: "{{ __trustee_server_trustee_packages }}" + + - name: Check trustee quadlet install directory exists + ansible.builtin.stat: + path: "{{ __trustee_server_quadlet_install_dir }}" + register: quadlet_dir + + - name: Assert quadlet directory exists + ansible.builtin.assert: + that: quadlet_dir.stat.exists + fail_msg: "Quadlet install directory {{ __trustee_server_quadlet_install_dir }} was not created" + + - name: Find quadlet files in install directory + ansible.builtin.find: + paths: "{{ __trustee_server_quadlet_install_dir }}" + patterns: + - "*.container" + - "*.volume" + - "*.network" + - "*.pod" + recurse: false + register: quadlet_files + + - name: Assert quadlet files exist + ansible.builtin.assert: + that: quadlet_files.matched | int > 0 + fail_msg: "No quadlet files found in {{ __trustee_server_quadlet_install_dir }}" + + - name: Check trustee certificates and keys were generated + ansible.builtin.stat: + path: "{{ item }}" + register: trustee_certs + loop: + - "{{ __trustee_server_config_dir }}/kbs/server.key" + - "{{ __trustee_server_config_dir }}/kbs/server.crt" + - "{{ __trustee_server_config_dir }}/kbs/auth.key" + - "{{ __trustee_server_config_dir }}/kbs/auth.pub" + - "{{ __trustee_server_config_dir }}/as/token.key" + - "{{ __trustee_server_config_dir }}/as/token.crt" + - "{{ __trustee_server_config_dir }}/kbs/trusted_certs/token0.crt" + + - name: Assert trustee certificates and keys exist + ansible.builtin.assert: + that: item.stat.exists + fail_msg: "Trustee cert/key {{ item.item }} was not generated" + loop: "{{ trustee_certs.results }}" + + - name: Find trustee pod file + ansible.builtin.find: + paths: "{{ __trustee_server_quadlet_install_dir }}" + patterns: "*.pod" + recurse: false + register: trustee_pod_files + + - name: Check trustee pod service is running + ansible.builtin.service: + name: "{{ (trustee_pod_files.files[0].path | basename) | replace('.pod', '') }}-pod" + state: started + check_mode: true \ No newline at end of file diff --git a/tests/tests_include_vars_from_parent.yml b/tests/tests_include_vars_from_parent.yml index 02b7831..f91ef14 100644 --- a/tests/tests_include_vars_from_parent.yml +++ b/tests/tests_include_vars_from_parent.yml @@ -44,7 +44,7 @@ import_role: name: caller vars: - roletoinclude: linux-system-roles.template + roletoinclude: linux-system-roles.trustee_server - name: Cleanup file: diff --git a/tests/tests_secret_registration_server.yml b/tests/tests_secret_registration_server.yml new file mode 100644 index 0000000..1deb2ac --- /dev/null +++ b/tests/tests_secret_registration_server.yml @@ -0,0 +1,39 @@ +# SPDX-License-Identifier: MIT +--- +- name: Ensure secret registration server is deployed when enabled + hosts: all + gather_facts: true + vars: + trustee_server_trustee: true + trustee_server_secret_registration_enabled: true + tasks: + - name: Include trustee_server role + ansible.builtin.include_role: + name: linux-system-roles.trustee_server + public: true + + - name: Gather package facts + ansible.builtin.package_facts: + manager: auto + + - name: Assert required packages are installed + ansible.builtin.assert: + that: item in ansible_facts.packages + fail_msg: "Required package {{ item }} is not installed" + loop: "{{ __trustee_server_trustee_packages + __trustee_server_secret_registration_packages }}" + + - name: Check secret registration server script exists + ansible.builtin.stat: + path: /usr/local/bin/secret_registration_server.py + register: __trustee_server_secret_reg_script + + - name: Assert secret registration server script was deployed + ansible.builtin.assert: + that: __trustee_server_secret_reg_script.stat.exists + fail_msg: "Secret registration server script was not deployed" + + - name: Check secret registration server service is running + ansible.builtin.service: + name: "secret_registration_server" + state: started + check_mode: true diff --git a/tests/vars/rh_distros_vars.yml b/tests/vars/rh_distros_vars.yml index 4347b7e..d9da062 100644 --- a/tests/vars/rh_distros_vars.yml +++ b/tests/vars/rh_distros_vars.yml @@ -1,20 +1,20 @@ # vars for handling conditionals for RedHat and clones # DO NOT EDIT - file is auto-generated # repo is https://github.com/linux-system-roles/.github -# file is playbooks/templates/tests/vars/rh_distros_vars.yml +# file is playbooks/trustee_server/tests/vars/rh_distros_vars.yml --- # Ansible distribution identifiers that the role treats like RHEL -__trustee_attestation_server_rh_distros: +__trustee_server_rh_distros: - AlmaLinux - CentOS - RedHat - Rocky # Same as above but includes Fedora -__trustee_attestation_server_rh_distros_fedora: "{{ __trustee_attestation_server_rh_distros + ['Fedora'] }}" +__trustee_server_rh_distros_fedora: "{{ __trustee_server_rh_distros + ['Fedora'] }}" # Use this in conditionals to check if distro is Red Hat or clone -__trustee_attestation_server_is_rh_distro: "{{ ansible_facts['distribution'] in __trustee_attestation_server_rh_distros }}" +__trustee_server_is_rh_distro: "{{ ansible_facts['distribution'] in __trustee_server_rh_distros }}" # Use this in conditionals to check if distro is Red Hat or clone, or Fedora -__trustee_attestation_server_is_rh_distro_fedora: "{{ ansible_facts['distribution'] in __trustee_attestation_server_rh_distros_fedora }}" +__trustee_server_is_rh_distro_fedora: "{{ ansible_facts['distribution'] in __trustee_server_rh_distros_fedora }}" diff --git a/vars/Fedora.yml b/vars/Fedora.yml index a783f79..c483078 100644 --- a/vars/Fedora.yml +++ b/vars/Fedora.yml @@ -2,6 +2,4 @@ --- # Put internal variables here with Fedora specific values. -# Example: -__template_packages: [] -__template_services: [] +__trustee_server_services: [] diff --git a/vars/RedHat_10.yml b/vars/RedHat_10.yml index c1a73a0..72ba4f9 100644 --- a/vars/RedHat_10.yml +++ b/vars/RedHat_10.yml @@ -2,6 +2,4 @@ --- # Put internal variables here with Red Hat Enterprise Linux 10 specific values. -# Example: -__template_packages: [] -__template_services: [] +__trustee_server_services: [] diff --git a/vars/RedHat_7.yml b/vars/RedHat_7.yml index 3815df4..f14c4dc 100644 --- a/vars/RedHat_7.yml +++ b/vars/RedHat_7.yml @@ -2,6 +2,4 @@ --- # Put internal variables here with Red Hat Enterprise Linux 7 specific values. -# Example: -__template_packages: [] -__template_services: [] +__trustee_server_services: [] diff --git a/vars/RedHat_8.yml b/vars/RedHat_8.yml index 954bf90..cb1472c 100644 --- a/vars/RedHat_8.yml +++ b/vars/RedHat_8.yml @@ -2,6 +2,4 @@ --- # Put internal variables here with Red Hat Enterprise Linux 8 specific values. -# Example: -__template_packages: [] -__template_services: [] +__trustee_server_services: [] diff --git a/vars/RedHat_9.yml b/vars/RedHat_9.yml index b367bff..c105b57 100644 --- a/vars/RedHat_9.yml +++ b/vars/RedHat_9.yml @@ -2,6 +2,4 @@ --- # Put internal variables here with Red Hat Enterprise Linux 9 specific values. -# Example: -__template_packages: [] -__template_services: [] +__trustee_server_services: [] diff --git a/vars/main.yml b/vars/main.yml index 9de2f49..3a35f06 100644 --- a/vars/main.yml +++ b/vars/main.yml @@ -5,11 +5,19 @@ # value in a platform/version specific file in vars/ # Examples of non-distribution specific (generic) internal variables: -__template_foo_config: foo.conf -__template_packages: [] -__template_services: [] +__trustee_server_trustee_packages: + - podman + - git +__trustee_server_services: [] +__trustee_server_secret_registration_packages: + - python3 +__trustee_server_quadlet_repo_url: "https://github.com/litian1992/trustee-quadlet-rhel.git" +__trustee_server_quadlet_repo_path: "quadlet" +__trustee_server_quadlet_repo_branch: "main" +__trustee_server_quadlet_install_dir: "/etc/containers/systemd" +__trustee_server_config_dir: "/etc/trustee" # ansible_facts required by the role -__template_required_facts: +__trustee_server_required_facts: - distribution - distribution_major_version - distribution_version @@ -17,23 +25,23 @@ __template_required_facts: # the subsets of ansible_facts that need to be gathered in case any of the # facts in required_facts is missing; see the documentation of # the 'gather_subset' parameter of the 'setup' module -__template_required_facts_subsets: "{{ ['!all', '!min'] + - __template_required_facts }}" +__trustee_server_required_facts_subsets: "{{ ['!all', '!min'] + + __trustee_server_required_facts }}" # BEGIN - DO NOT EDIT THIS BLOCK - rh distros variables # Ansible distribution identifiers that the role treats like RHEL -__template_rh_distros: +__trustee_server_rh_distros: - AlmaLinux - CentOS - RedHat - Rocky # Same as above but includes Fedora -__template_rh_distros_fedora: "{{ __template_rh_distros + ['Fedora'] }}" +__trustee_server_rh_distros_fedora: "{{ __trustee_server_rh_distros + ['Fedora'] }}" # Use this in conditionals to check if distro is Red Hat or clone -__template_is_rh_distro: "{{ ansible_facts['distribution'] in __template_rh_distros }}" +__trustee_server_is_rh_distro: "{{ ansible_facts['distribution'] in __trustee_server_rh_distros }}" # Use this in conditionals to check if distro is Red Hat or clone, or Fedora -__template_is_rh_distro_fedora: "{{ ansible_facts['distribution'] in __template_rh_distros_fedora }}" +__trustee_server_is_rh_distro_fedora: "{{ ansible_facts['distribution'] in __trustee_server_rh_distros_fedora }}" # END - DO NOT EDIT THIS BLOCK - rh distros variables