Skip to content

Lcs 1184 auto renewal for ssl certs and enable ssl health check - #321

Open
mrlockstar wants to merge 2 commits into
mainfrom
LCS-1184-Auto-renewal-for-SSL-certs-and-enable-SSL-health-check
Open

mrlockstar wants to merge 2 commits into
mainfrom
LCS-1184-Auto-renewal-for-SSL-certs-and-enable-SSL-health-check

Conversation

@mrlockstar

@mrlockstar mrlockstar commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Description

This was based on a script from another project; however, it did not work the exact way we wanted it to so it was modified via AI and tested using the lungcs environment.

It was used to renew both the preprod and prod certificate (there was no risk to live service).

image

https://github.com/DFE-Digital/teacher-services-cloud/blob/main/.github/workflows/afd-domain-renewal.yml

Evidence if it working can be seen here: -

https://dev.azure.com/nhse-dtos/lung-cancer-screening/_build?definitionId=142&_a=summary

preprod
https://dev.azure.com/nhse-dtos/lung-cancer-screening/_build/results?buildId=46397&view=results

prod
https://dev.azure.com/nhse-dtos/lung-cancer-screening/_build/results?buildId=46388&view=results

Context

Type of changes

  • Refactoring (non-breaking change)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would change existing functionality)
  • Bug fix (non-breaking change which fixes an issue)

Checklist

  • I am familiar with the contributing guidelines
  • I have followed the code style of the project
  • I have added tests to cover my changes
  • I have updated the documentation accordingly
  • This PR is a result of pair or mob programming

Sensitive Information Declaration

To ensure the utmost confidentiality and protect your and others privacy, we kindly ask you to NOT including PII (Personal Identifiable Information) / PID (Personal Identifiable Data) or any other sensitive data in this PR (Pull Request) and the codebase changes. We will remove any PR that do contain any sensitive information. We really appreciate your cooperation in this matter.

  • I confirm that neither PII/PID nor sensitive data are included in this PR and the codebase changes.

@mrlockstar
mrlockstar force-pushed the LCS-1184-Auto-renewal-for-SSL-certs-and-enable-SSL-health-check branch 2 times, most recently from f9adf4c to 77a552e Compare October 2, 2026 10:16
The SSL certificates are semi managed in Front door and you can renew the certificates via the console; however, this does not happen automatically for the apec domain and instead a manual check needs to occur. Please note that this script is triggered off the ADO pipeline and uses the MI that is used to deploy the terraform in Azure.
@mrlockstar
mrlockstar force-pushed the LCS-1184-Auto-renewal-for-SSL-certs-and-enable-SSL-health-check branch from 77a552e to f184524 Compare October 2, 2026 10:18
Comment on lines +1 to +193
#!/bin/bash

################################################################################
# Azure Front Door SSL Certificate Renewal - Process Flow
################################################################################
#
# 1. Receive the Azure subscription, target domain and certificate expiry
# threshold as command-line arguments.
#
# 2. Find the Azure Front Door profile containing the requested custom domain.
#
# 3. Read the domain's current Front Door configuration and validation state.
#
# 4. Connect to the target domain over HTTPS and determine the expiry date
# of the TLS certificate currently being served.
#
# 5. Decide whether certificate renewal/revalidation is required:
#
# - If forced with -f:
# Regenerate the Front Door validation token.
#
# - If Front Door is in PendingRevalidation or InternalError:
# Regenerate the validation token.
#
# - If the currently served TLS certificate expires within the configured
# threshold:
# Regenerate the Front Door validation token.
#
# - Otherwise:
# Make no changes and finish.
#
# 6. When regeneration is required:
# - Request a new Front Door validation token.
# - Wait for Front Door to expose the refreshed token.
# - Retrieve the new token and current validation state.
#
# 7. If DNS validation is required:
# - Locate the Azure DNS zone for the domain.
# - Determine the required _dnsauth TXT record.
# - Check whether the TXT record already exists.
# - Create the record if it does not exist.
# - Update it if the existing token differs from the Front Door token.
# - Leave it unchanged if it already contains the correct token.
#
# 8. If Front Door validation is Pending or PendingRevalidation:
# - Wait for the validation state to become Approved.
# - Fail if Front Door enters a terminal error state or the timeout
# is reached.
#
# 9. Finish after the domain is validated or when no renewal/revalidation
# was required.
#
# IMPORTANT:
# The renewal decision is based on the expiry of the TLS certificate
# currently served by the domain. The validation-token expiry alone does
# not trigger certificate regeneration.
#
################################################################################


set -euo pipefail

TODAY=$(TZ=Europe/London date -Idate)

################################################################################
# Parse command-line arguments
################################################################################

usage() {
echo "Usage: $0 -s <subscription> -d <target-domain> -e <expiration-days> [-r <resource-group>]"
echo
echo "Required arguments:"
echo " -s Azure subscription name or ID"
echo " -d Target domain"
echo " -e TLS certificate expiration threshold in days"
echo
echo "Optional arguments:"
echo " -f Force regeneration of TLS certificate (optional)"
echo " -r Resource group filter"
echo
echo "Example:"
echo " $0 -s my-subscription -d example.nhs.uk -e 30"
echo " $0 -s my-subscription -d example.nhs.uk -e 30 -f"
exit 1
}

AZ_SUBSCRIPTION_SCOPE=""
TARGET_DOMAIN=""
AFD_DOMAIN_EXPIRATION_DAYS=""
RESOURCE_GROUP_FILTER=""
FORCE_REGENERATION=false

while getopts ":s:d:e:fr:h" opt; do
case "$opt" in
s)
AZ_SUBSCRIPTION_SCOPE="$OPTARG"
;;
d)
TARGET_DOMAIN="$OPTARG"
;;
e)
AFD_DOMAIN_EXPIRATION_DAYS="$OPTARG"
;;
f)
FORCE_REGENERATION=true
;;
r)
RESOURCE_GROUP_FILTER="$OPTARG"
;;
h)
usage
;;
:)
echo "ERROR: Option -$OPTARG requires an argument." >&2
usage
;;
\?)
echo "ERROR: Invalid option: -$OPTARG" >&2
usage
;;
esac
done

################################################################################
# Validate configuration
################################################################################

if [[ -z "$AZ_SUBSCRIPTION_SCOPE" ]]; then
echo "ERROR: Azure subscription is required. Use -s <subscription>." >&2
usage
fi

if [[ -z "$TARGET_DOMAIN" ]]; then
echo "ERROR: Target domain is required. Use -d <domain>." >&2
usage
fi

if [[ -z "$AFD_DOMAIN_EXPIRATION_DAYS" ]]; then
echo "ERROR: Expiration days is required. Use -e <days>." >&2
usage
fi

if ! [[ "$AFD_DOMAIN_EXPIRATION_DAYS" =~ ^[0-9]+$ ]]; then
echo "ERROR: Expiration days must be a positive integer." >&2
echo "Value supplied: $AFD_DOMAIN_EXPIRATION_DAYS" >&2
exit 1
fi

echo "TLS certificate expiration threshold: $AFD_DOMAIN_EXPIRATION_DAYS days"
echo "Target domain: $TARGET_DOMAIN"
echo "Azure subscription: $AZ_SUBSCRIPTION_SCOPE"

if [[ -n "$RESOURCE_GROUP_FILTER" ]]; then
echo "Resource group filter: $RESOURCE_GROUP_FILTER"
fi

echo

################################################################################
# Find Azure Front Door profiles
################################################################################

echo "Looking for Azure Front Door CDNs..."

AFD_LIST=$(
az afd profile list \
--only-show-errors \
--subscription "$AZ_SUBSCRIPTION_SCOPE" |
jq -rc '.[] | {
"name": .name,
"resourceGroup": .resourceGroup
}'
)

AFD_COUNT=$(echo "$AFD_LIST" | wc -l | tr -d ' ')

echo "Found $AFD_COUNT Azure Front Door(s) total"
echo

if [[ -n "$RESOURCE_GROUP_FILTER" ]]; then
echo "Filtering for resource group: $RESOURCE_GROUP_FILTER"
echo
fi

################################################################################
# Find the Azure Front Door containing TARGET_DOMAIN
################################################################################

TARGET_AFD=""
TARGET_RESOURCE_GROUP=""
MATCHING_DOMAIN=""

for AZURE_FRONT_DOOR in $AFD_LIST; do

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it not be better to use something like
while IFS= read -r AZURE_FRONT_DOOR; do ... done <<< "$AFD_LIST"

given that it looks like AFD_LIST contains compact JSON objects?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Happy to try that

echo "FORCE regeneration enabled."
echo "Validation token will be regenerated regardless of certificate expiry or validation state."

elif [[ "$STATE" == "PendingRevalidation" || "$STATE" == "InternalError" ]]; then

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

echo "Checking currently served TLS certificate..."

CERT_END_DATE=$(
echo | openssl s_client \

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we run this script in an unattended mode, then we'll need some kind of timeout for requests that take too long. So possibly one could add timeout value or similar?

@sonarqubecloud

sonarqubecloud Bot commented Oct 2, 2026

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants