diff --git a/.github/workflows/check-certificates.yml b/.github/workflows/check-certificates.yml
new file mode 100644
index 000000000..a3fe636ef
--- /dev/null
+++ b/.github/workflows/check-certificates.yml
@@ -0,0 +1,114 @@
+name: Check for issues with signing certificates
+
+on:
+  schedule:
+    # run every 10 hours
+    - cron: "0 */10 * * *"
+  # workflow_dispatch event allows the workflow to be triggered manually.
+  # This could be used to run an immediate check after updating certificate secrets.
+  # See: https://docs.github.com/en/actions/reference/events-that-trigger-workflows#workflow_dispatch
+  workflow_dispatch:
+
+env:
+  # Begin notifications when there are less than this many days remaining before expiration
+  EXPIRATION_WARNING_PERIOD: 30
+
+jobs:
+  check-certificates:
+    runs-on: ubuntu-latest
+
+    strategy:
+      fail-fast: false
+
+      matrix:
+        certificate:
+          - identifier: macOS signing certificate # Text used to identify the certificate in notifications
+            certificate-secret: INSTALLER_CERT_MAC_P12  # The name of the secret that contains the certificate
+            password-secret: INSTALLER_CERT_MAC_PASSWORD  # The name of the secret that contains the certificate password
+          - identifier: Windows signing certificate
+            certificate-secret: INSTALLER_CERT_WINDOWS_PFX
+            password-secret: INSTALLER_CERT_WINDOWS_PASSWORD
+
+    steps:
+      - name: Set certificate path environment variable
+        run: |
+          # See: https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-commands-for-github-actions#setting-an-environment-variable
+          echo "CERTIFICATE_PATH=${{ runner.temp }}/certificate.p12" >> "$GITHUB_ENV"
+      - name: Decode certificate
+        env:
+          CERTIFICATE: ${{ secrets[matrix.certificate.certificate-secret] }}
+        run: |
+          echo "${{ env.CERTIFICATE }}" | base64 --decode > "${{ env.CERTIFICATE_PATH }}"
+      - name: Verify certificate
+        env:
+          CERTIFICATE_PASSWORD: ${{ secrets[matrix.certificate.password-secret] }}
+        run: |
+          (
+            openssl pkcs12 \
+              -in "${{ env.CERTIFICATE_PATH }}" \
+              -noout -passin env:CERTIFICATE_PASSWORD
+          ) || (
+            echo "::error::Verification of ${{ matrix.certificate.identifier }} failed!!!"
+            exit 1
+          )
+      # See: https://github.com/rtCamp/action-slack-notify
+      - name: Slack notification of certificate verification failure
+        if: failure()
+        uses: rtCamp/action-slack-notify@v2.1.0
+        env:
+          SLACK_WEBHOOK: ${{ secrets.TEAM_CREATE_CHANNEL_SLACK_WEBHOOK }}
+          SLACK_MESSAGE: |
+            :warning::warning::warning::warning:
+            WARNING: ${{ github.repository }} ${{ matrix.certificate.identifier }} verification failed!!!
+            :warning::warning::warning::warning:
+          SLACK_COLOR: danger
+          MSG_MINIMAL: true
+
+      - name: Get days remaining before certificate expiration date
+        env:
+          CERTIFICATE_PASSWORD: ${{ secrets[matrix.certificate.password-secret] }}
+        id: get-days-before-expiration
+        run: |
+          EXPIRATION_DATE="$(
+            (
+              openssl pkcs12 \
+                -in "${{ env.CERTIFICATE_PATH }}" \
+                -clcerts \
+                -nodes \
+                -passin env:CERTIFICATE_PASSWORD
+            ) | (
+              openssl x509 \
+                -noout \
+                -enddate
+            ) | (
+              grep \
+                --max-count=1 \
+                --only-matching \
+                --perl-regexp \
+                'notAfter=(\K.*)'
+            )
+          )"
+          DAYS_BEFORE_EXPIRATION="$((($(date --utc --date="$EXPIRATION_DATE" +%s) - $(date --utc +%s)) / 60 / 60 / 24))"
+          # Display the expiration information in the log
+          echo "Certificate expiration date: $EXPIRATION_DATE"
+          echo "Days remaining before expiration: $DAYS_BEFORE_EXPIRATION"
+          echo "::set-output name=days::$DAYS_BEFORE_EXPIRATION"
+      - name: Check if expiration notification period has been reached
+        id: check-expiration
+        run: |
+          if [[ ${{ steps.get-days-before-expiration.outputs.days }} -lt ${{ env.EXPIRATION_WARNING_PERIOD }} ]]; then
+            echo "::error::${{ matrix.certificate.identifier }} will expire in ${{ steps.get-days-before-expiration.outputs.days }} days!!!"
+            exit 1
+          fi
+      - name: Slack notification of pending certificate expiration
+        # Don't send spurious expiration notification if verification fails
+        if: failure() && steps.check-expiration.outcome == 'failure'
+        uses: rtCamp/action-slack-notify@v2.1.0
+        env:
+          SLACK_WEBHOOK: ${{ secrets.TEAM_CREATE_CHANNEL_SLACK_WEBHOOK }}
+          SLACK_MESSAGE: |
+            :warning::warning::warning::warning:
+            WARNING: ${{ github.repository }} ${{ matrix.certificate.identifier }} will expire in ${{ steps.get-days-before-expiration.outputs.days }} days!!!
+            :warning::warning::warning::warning:
+          SLACK_COLOR: danger
+          MSG_MINIMAL: true
\ No newline at end of file