Let’s Encrypt Certificate Renewal Fails in Trellis with KeyError: 'contact'

My site has been running without issues for months, but today I began seeing a “Your connection is not private” error on the production site. When I attempted to reprovision, I encountered the following error:

TASK [letsencrypt : Generate the certificates] ***********************************************************************************************************************************
fatal: [XXX.XX.XXX.XXX]: FAILED! => {"changed": false, "cmd": ["./renew-certs.py"], "delta": "0:00:03.683988", "end": "2025-07-06 13:01:54.058232", "msg": "non-zero return code", "rc": 1, "start": "2025-07-06 13:01:50.374244", "stderr": "Error while generating certificate for example.com\nTraceback (most recent call last):\n  File \"/usr/local/letsencrypt/acme_tiny.py\", line 198, in <module>\n    main(sys.argv[1:])\n  File \"/usr/local/letsencrypt/acme_tiny.py\", line 194, in main\n    signed_crt = get_crt(args.account_key, args.csr, args.acme_dir, log=LOGGER, CA=args.ca, disable_check=args.disable_check, directory_url=args.directory_url, contact=args.contact)\n                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"/usr/local/letsencrypt/acme_tiny.py\", line 116, in get_crt\n    log.info(\"Updated contact details:\\n{0}\".format(\"\\n\".join(account['contact'])))\n                                                              ~~~~~~~^^^^^^^^^^^\nKeyError: 'contact'", "stderr_lines": ["Error while generating certificate for example.com", "Traceback (most recent call last):", "  File \"/usr/local/letsencrypt/acme_tiny.py\", line 198, in <module>", "    main(sys.argv[1:])", "  File \"/usr/local/letsencrypt/acme_tiny.py\", line 194, in main", "    signed_crt = get_crt(args.account_key, args.csr, args.acme_dir, log=LOGGER, CA=args.ca, disable_check=args.disable_check, directory_url=args.directory_url, contact=args.contact)", "                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^", "  File \"/usr/local/letsencrypt/acme_tiny.py\", line 116, in get_crt", "    log.info(\"Updated contact details:\\n{0}\".format(\"\\n\".join(account['contact'])))", "                                                              ~~~~~~~^^^^^^^^^^^", "KeyError: 'contact'"], "stdout": "", "stdout_lines": []}

I suspect this is related to a recent change in the ACME protocol responses, as discussed in this Reddit thread:
https://www.reddit.com/r/letsencrypt/comments/1lhxi9x/fyi_acmetiny_contact_switch_now_breaks_with_le/

I removed the following line (35) from trellis/roles/letsencrypt/templates/renew-certs.py and the certificates were successfully renewed after reprovisioning:

'--contact {{ letsencrypt_contact_emails | map('regex_replace', '(^.*$)', 'mailto:\\1') | join (' ') }} '

Relevant:

This earlier Trellis change would also solve this issue: https://github.com/roots/trellis/pull/1558/files

Highly recommend getting some sort of SSL monitoring in place if you’re managing your own servers. I’ve been using updown.io for about a decade and it’s super cheap. Use https://updown.io/r/xcnzb if you want 100k free credits

The roots.io server is a bit behind on Trellis changes and also had this issue. Got the heads up about it last night because of my alerts: