From 957eb9af88df1c49ccd05dd9d38110eb84f4d32b Mon Sep 17 00:00:00 2001 From: Andreas Hering Date: Thu, 23 Mar 2023 13:51:39 +0100 Subject: [PATCH] Add Azure blob storage provider (#89) * Trigger tests * Trigger more tests * Add azure block storage provider * Use FQCN * Adjust file permissions * Link to service principal creation docs * Remove prerequisite not needed when using azbs provider * Add azure.azcollection dependency * Fix missing loop/when part --------- Co-authored-by: Andreas Hering --- docs/http-challenge/azbs.md | 76 ++++++++++++++++++++ galaxy.yml | 1 + roles/acme/tasks/challenge/http-01/azbs.yml | 77 +++++++++++++++++++++ 3 files changed, 154 insertions(+) create mode 100644 docs/http-challenge/azbs.md create mode 100644 roles/acme/tasks/challenge/http-01/azbs.yml diff --git a/docs/http-challenge/azbs.md b/docs/http-challenge/azbs.md new file mode 100644 index 0000000..ebdc24a --- /dev/null +++ b/docs/http-challenge/azbs.md @@ -0,0 +1,76 @@ +# Variables for Azure blob storage http-challenge + +| Variable | Required | Default | Description +|-----------------------|----------|-----------|------------ +| acme_azbs_resource_group | yes | | Name of the Azure resource group to which the storage account has been allocated +| acme_azbs_storage_account_name | yes | | Azure storage account name which should be used +| acme_azbs_container_name | yes | | Azure container name which will be used/created in Azure storage account +| acme_azbs_subscription_id | yes | | Azure subscription id +| acme_azbs_client_id | yes | | Client ID of service principal/application +| acme_azbs_secret | yes | | Value of secret of service principal/application (Note: not the ID) +| acme_azbs_tenant_id | yes | | Tenant ID of service principal/application + +## Validation + +You have to create a service principal/application in the Azure Active Directory. +This can be done via Frontend, Azure CLI or terraform. +See https://learn.microsoft.com/en-us/azure/active-directory/develop/app-objects-and-service-principals + +You also have to set a redirect rule in your proxy or webserver to allow the acme challenge bot to read the file, during the http-01 challenge to work. + +*Please note that the URL for the storage account and container needs to be adjusted to the name of your used account and container.* + +**HaProxy:** +*works with version >= 1.6* + +```bash +http-request redirect code 301 location https://your-storage-account-name.blob.core.windows.net[url,regsub(^/.well-known/acme-challenge,/my-containername,)] if { path_beg /.well-known/acme-challenge } +``` + +(can be set in frontend or backend definition) + +**Apache:** + +```bash +RewriteRule \.well-known/acme-challenge/(.*) https://your-storage-account-name.blob.core.windows.net/your-container-name/$1 +``` + +**Nginx:** + +```bash +rewrite \.well-known/acme-challenge/(.*) https://your-storage-account-name.blob.core.windows.net/your-container-name/$1 +``` + +## Usage + +> you should think about encrypting all azure account infos + +```yaml +- name: create the certificate for example.com + hosts: localhost + collections: + - t_systems_mms.acme + roles: + - acme + vars: + acme_domain: + certificate_name: "example.com" + zone: "example.com" + email_address: "ssl-admin@example.com" + subject_alt_name: + - example.com + - domain1.example.com + - domain2.example.com + acme_challenge_provider: "azbs" + acme_use_live_directory: false + acme_account_email: "ssl-admin@example.com" + acme_azbs_resource_group: "my-resource-group" + acme_azbs_storage_account_name: "my-storage-account-name" + acme_azbs_container_name: "my-container" + acme_azbs_subscription_id: "0000-11111-2222-3333-444444" + acme_azbs_client_id: "1234-21231-14152-1231" + acme_azbs_secret: !vault | + $ANSIBLE_VAULT;1.1;AES256 + ... + acme_azbs_tenant_id: "2132184-3534543-54354-3543" +``` diff --git a/galaxy.yml b/galaxy.yml index f9d3a6b..f51041d 100644 --- a/galaxy.yml +++ b/galaxy.yml @@ -23,6 +23,7 @@ dependencies: community.crypto: '>=1.0.0' openstack.cloud: '>=1.2.1' amazon.aws: '>=5.0.0' + azure.azcollection: '>=1.14.0' repository: 'https://github.com/T-Systems-MMS/ansible-collection-acme' documentation: 'https://github.com/T-Systems-MMS/ansible-collection-acme' homepage: 'https://github.com/T-Systems-MMS/ansible-collection-acme' diff --git a/roles/acme/tasks/challenge/http-01/azbs.yml b/roles/acme/tasks/challenge/http-01/azbs.yml new file mode 100644 index 0000000..be59385 --- /dev/null +++ b/roles/acme/tasks/challenge/http-01/azbs.yml @@ -0,0 +1,77 @@ +--- +- name: Validate challenge only if it is created or changed # noqa no-handler + when: challenge is changed + block: + - name: Create challenge file with SAN domain for azure blob storage upload # noqa template-instead-of-copy + ansible.builtin.copy: + dest: "acme-challenge.{{ item }}" + content: "{{ challenge['challenge_data'][item]['http-01']['resource_value'] }}" + mode: 0640 + loop: "{{ acme_domain.subject_alt_name }}" + when: + - acme_domain.subject_alt_name is defined + # only runs if the challenge is run the first time, because then there is challenge_data + - challenge['challenge_data'][item] is defined + + - name: Create storage container and upload challenge file to it + azure.azcollection.azure_rm_storageblob: + resource_group: "{{ acme_azbs_resource_group }}" + storage_account_name: "{{ acme_azbs_storage_account_name }}" + public_access: "blob" + container: "{{ acme_azbs_container_name }}" + blob: "{{ challenge['challenge_data'][item]['http-01']['resource'] }}" + src: "acme-challenge.{{ item }}" + content_type: 'text/plain' # _type or _encoding have to be set + subscription_id: "{{ acme_azbs_subscription_id }}" + client_id: "{{ acme_azbs_client_id }}" + secret: "{{ acme_azbs_secret }}" + tenant: "{{ acme_azbs_tenant_id }}" + loop: "{{ acme_domain.subject_alt_name }}" + when: + - acme_domain.subject_alt_name is defined + # only runs if the challenge is run the first time, because then there is challenge_data + - challenge['challenge_data'][item] is defined + + # validate certficate + - name: Let the challenge be validated and retrieve the cert and intermediate certificate + community.crypto.acme_certificate: + account_key_src: "{{ acme_account_key_path }}" + account_email: "{{ acme_account_email }}" + csr: "{{ acme_csr_path }}" + cert: "{{ acme_cert_path }}" + fullchain: "{{ acme_fullchain_path }}" + chain: "{{ acme_intermediate_path }}" + challenge: http-01 + force: "{{ acme_force_renewal | default(false) }}" + acme_directory: "{{ acme_directory }}" + acme_version: 2 + terms_agreed: true + remaining_days: "{{ acme_remaining_days }}" + data: "{{ challenge }}" + + - name: Remove challenge file for SAN domain from azure blob storage container + azure.azcollection.azure_rm_storageblob: + resource_group: "{{ acme_azbs_resource_group }}" + storage_account_name: "{{ acme_azbs_storage_account_name }}" + container: "{{ acme_azbs_container_name }}" + blob: "{{ challenge['challenge_data'][item]['http-01']['resource'] }}" + state: absent + subscription_id: "{{ acme_azbs_subscription_id }}" + client_id: "{{ acme_azbs_client_id }}" + secret: "{{ acme_azbs_secret }}" + tenant: "{{ acme_azbs_tenant_id }}" + loop: "{{ acme_domain.subject_alt_name }}" + when: + - acme_domain.subject_alt_name is defined + # only runs if the challenge is run the first time, because then there is challenge_data + - challenge['challenge_data'][item] is defined + + - name: Remove challenge file for SAN domain from fs + ansible.builtin.file: + dest: "acme-challenge.{{ item }}" + state: absent + loop: "{{ acme_domain.subject_alt_name }}" + when: + - acme_domain.subject_alt_name is defined + # only runs if the challenge is run the first time, because then there is challenge_data + - challenge['challenge_data'][item] is defined