From 3df3ff6584f1c64e7afa11c1255cb05de9363109 Mon Sep 17 00:00:00 2001 From: L3D Date: Thu, 19 Sep 2024 12:21:08 +0200 Subject: [PATCH] Create INWX API ACME Scripts Hey there, I created a script to add get certificated with getssl using the [INWX Domrobot Python 3 Client](https://github.com/inwx/python-client) --- dns_scripts/INWX-README.md | 62 +++++++++++++++++++++ dns_scripts/dns_add_inwx.py | 93 +++++++++++++++++++++++++++++++ dns_scripts/dns_del_inwx.py | 106 ++++++++++++++++++++++++++++++++++++ 3 files changed, 261 insertions(+) create mode 100644 dns_scripts/INWX-README.md create mode 100755 dns_scripts/dns_add_inwx.py create mode 100755 dns_scripts/dns_del_inwx.py diff --git a/dns_scripts/INWX-README.md b/dns_scripts/INWX-README.md new file mode 100644 index 00000000..19ab059b --- /dev/null +++ b/dns_scripts/INWX-README.md @@ -0,0 +1,62 @@ +## Using INWX DNS for LetsEncrypt domain validation + +### Install Requirements + +The INWX API Python3 script requires two Python packages: + +```bash +pip3 install INWX.Domrobot tldextract +``` + +You could install it for the user running getssl, or you could create a python3 venv. + +```bash +# install python3 venv apt packages +sudo apt install python3 python3-venv + +# Create venv +python3 -m venv venv + +# activate venv +source venv/bin/activate + +# install requirements +pip3 install INWX.Domrobot tldextract +``` + +If you are installing the Python packages in venv, you should make sure that you either +you either enable the venv before running getssl, or you +add the venv to the ``DNS_ADD_COMMAND'' and ``DNS_DEL_COMMAND'' commands. +See example below. + +### Enabling the scripts + +Set the following options in `getssl.cfg` (either global or domain-specific): + +``` +VALIDATE_VIA_DNS="true" +DNS_ADD_COMMAND="/usr/share/getssl/dns_scripts/dns_add_inwx.py" +DNS_DEL_COMMAND="/usr/share/getssl/dns_scripts/dns_del_inwx.py" +``` + +If you are using a python3 venv as described above, this is an example of how to include it: + +``` +VALIDATE_VIA_DNS="true" +DNS_ADD_COMMAND="/path/to/venv/bin/python3 /usr/share/getssl/dns_scripts/dns_add_inwx.py" +DNS_DEL_COMMAND="/path/to/venv/bin/python3 /usr/share/getssl/dns_scripts/dns_del_inwx.py" +``` + +*Obviously the "/path/to/venv" needs to be replaced with the actual path to your venv, e.g. "/home/getssl/venv".* + +### Authentication + +Your INWX credentials will be used to authenticate to INWX. +If you are using a second factor, please have a look at the [INWX Domrobot Pthon3 Client](https://github.com/inwx/python-client) as it is currently not implemented in the inwx api script. + +Set the following options in the domain-specific `getssl.cfg` or make sure these enviroment variables are present. + +``` +export INWX_USERNAME="your_inwx_username" +export INWX_PASSWORD="..." +``` diff --git a/dns_scripts/dns_add_inwx.py b/dns_scripts/dns_add_inwx.py new file mode 100755 index 00000000..cfa2aaee --- /dev/null +++ b/dns_scripts/dns_add_inwx.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Script to Set TXT Record at INWX using the API +This script requires the pip packages INWX.Domrobot and tldextract +This script is using enviroment variables to get inwx credentials +""" + +import sys +import os +import argparse +from INWX.Domrobot import ApiClient +import tldextract + +# Create Parser-Objekt +parser = argparse.ArgumentParser( + description='Using the INWX API to change DNS TXT Records for the ACME DNS-01 Challange', + epilog= "The environment variables 'INWX_USERNAME' and 'INWX_PASSWORD' are required too") + +# Adding Args +parser.add_argument('fulldomain', type=str, help='The full domain to add TXT Record.') +parser.add_argument('token', type=str, help='The ACME DNS-01 token.') +parser.add_argument('--debug', action='store_true', help='Enable debug mode.') + +# Parsing Args +args = parser.parse_args() +INWX_FULLDOMAIN = args.fulldomain +ACME_TOKEN = args.token +DEBUG = args.debug + +# Parsing ENV +INWX_USERNAME = os.getenv('INWX_USERNAME', '') +INWX_PASSWORD = os.getenv('INWX_PASSWORD', '') + +# Splitting Domain +domain = tldextract.extract(INWX_FULLDOMAIN) +INWX_SUBDOMAIN = domain.subdomain +INWX_DOMAIN = f"{domain.domain}.{domain.suffix}" + +# Check if either environment variable is empty and handle the error +if not INWX_USERNAME or not INWX_PASSWORD: + print("Error: The following environment variables are required and cannot be empty:") + if not INWX_USERNAME: + print(" - INWX_USERNAME: Your INWX account username.") + if not INWX_PASSWORD: + print(" - INWX_PASSWORD: Your INWX account password.") + sys.exit(1) + +if DEBUG: + print(f'FQDN: {INWX_FULLDOMAIN}') + print(f'Domain: {INWX_DOMAIN}') + print(f'Subdomain: {INWX_SUBDOMAIN}') + print(f'Token: {ACME_TOKEN}') + print(f'User: {INWX_USERNAME}') + print(f'Password: {INWX_PASSWORD}') + +# By default the ApiClient uses the test api (OT&E). +# If you want to use the production/live api we have a +# constant named API_LIVE_URL in the ApiClient class. +# Just set api_url=ApiClient.API_LIVE_URL and you're good. +# api_client = ApiClient(api_url=ApiClient.API_OTE_URL, debug_mode=DEBUG) +api_client = ApiClient(api_url=ApiClient.API_LIVE_URL, debug_mode=DEBUG) + +# If you have 2fa enabled, take a look at the documentation of the ApiClient#login method +# to get further information about the login, especially the shared_secret parameter. +login_result = api_client.login(INWX_USERNAME, INWX_PASSWORD) + +# login was successful +if login_result['code'] == 1000: + + # Make an api call and save the result in a variable. + # We want to create a new nameserver record, so we call the api method nameserver.createRecord. + # See https://www.inwx.de/en/help/apidoc/f/ch02s15.html#nameserver.createRecord for parameters + # ApiClient#call_api returns the api response as a dict. + if INWX_SUBDOMAIN == '': + domain_entry_result = api_client.call_api(api_method='nameserver.createRecord', method_params={'domain': INWX_DOMAIN, 'name': '_acme-challenge', 'type': 'TXT', 'content': ACME_TOKEN}) # pylint: disable=C0301 + else: + domain_entry_result = api_client.call_api(api_method='nameserver.createRecord', method_params={'domain': INWX_DOMAIN, 'name': f'_acme-challenge.{INWX_SUBDOMAIN}', 'type': 'TXT', 'content': ACME_TOKEN}) # pylint: disable=C0301 + + # With or without successful check, we perform a logout. + api_client.logout() + + # validating return code + if domain_entry_result['code'] == 2302: + sys.exit(f"{domain_entry_result['msg']}.\nTry nameserver.updateRecord or nameserver.deleteRecord instead") # pylint: disable=C0301 + elif domain_entry_result['code'] == 1000: + if DEBUG: + print(domain_entry_result['msg']) + sys.exit() + else: + sys.exit(domain_entry_result) +else: + sys.exit('Api login error. Code: ' + str(login_result['code']) + ' Message: ' + login_result['msg']) # pylint: disable=C0301 diff --git a/dns_scripts/dns_del_inwx.py b/dns_scripts/dns_del_inwx.py new file mode 100755 index 00000000..b7d57ff8 --- /dev/null +++ b/dns_scripts/dns_del_inwx.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Script to remove TXT Record at INWX using the API +This script requires the pip packages INWX.Domrobot and tldextract +This script is using enviroment variables to get inwx credentials +""" + +import sys +import os +import argparse +from INWX.Domrobot import ApiClient +import tldextract + +# Create Parser-Objekt +parser = argparse.ArgumentParser( + description='Using the INWX API to remove DNS TXT Records for the ACME DNS-01 Challange', + epilog= "The environment variables 'INWX_USERNAME' and 'INWX_PASSWORD' are required too") + +# Adding Args +parser.add_argument('fulldomain', type=str, help='The full domain to add TXT Record.') +parser.add_argument('token', type=str, help='The ACME DNS-01 token.') +parser.add_argument('--debug', action='store_true', help='Enable debug mode.') + +# Parsing Args +args = parser.parse_args() +INWX_FULLDOMAIN = args.fulldomain +ACME_TOKEN = args.token +DEBUG = args.debug + +# Parsing ENV +INWX_USERNAME = os.getenv('INWX_USERNAME', '') +INWX_PASSWORD = os.getenv('INWX_PASSWORD', '') + +# Splitting Domain +domain = tldextract.extract(INWX_FULLDOMAIN) +INWX_SUBDOMAIN = domain.subdomain +INWX_DOMAIN = f"{domain.domain}.{domain.suffix}" + +# Check if either environment variable is empty and handle the error +if not INWX_USERNAME or not INWX_PASSWORD: + print("Error: The following environment variables are required and cannot be empty:") + if not INWX_USERNAME: + print(" - INWX_USERNAME: Your INWX account username.") + if not INWX_PASSWORD: + print(" - INWX_PASSWORD: Your INWX account password.") + sys.exit(1) + +if DEBUG: + print(f'FQDN: {INWX_FULLDOMAIN}') + print(f'Domain: {INWX_DOMAIN}') + print(f'Subdomain: {INWX_SUBDOMAIN}') + print(f'Token: {ACME_TOKEN}') + print(f'User: {INWX_USERNAME}') + print(f'Password: {INWX_PASSWORD}') + +# By default the ApiClient uses the test api (OT&E). +# If you want to use the production/live api we have a +# constant named API_LIVE_URL in the ApiClient class. +# Just set api_url=ApiClient.API_LIVE_URL and you're good. +# api_client = ApiClient(api_url=ApiClient.API_OTE_URL, debug_mode=DEBUG) +api_client = ApiClient(api_url=ApiClient.API_LIVE_URL, debug_mode=DEBUG) + +# If you have 2fa enabled, take a look at the documentation of the ApiClient#login method +# to get further information about the login, especially the shared_secret parameter. +login_result = api_client.login(INWX_USERNAME, INWX_PASSWORD) + +# login was successful +if login_result['code'] == 1000: + + # Make an api call and save the result in a variable. + # We want to get a the id of the _acme-challange TXT record, + # so we call the api method nameserver.info. + # See https://www.inwx.de/en/help/apidoc/f/ch02s15.html#nameserver.info for parameters + # ApiClient#call_api returns the api response as a dict. + if INWX_SUBDOMAIN == '': + domain_info_result = api_client.call_api(api_method='nameserver.info', method_params={'domain': INWX_DOMAIN, 'name': '_acme-challenge', 'type': 'TXT', 'content': ACME_TOKEN}) # pylint: disable=C0301 + else: + domain_info_result = api_client.call_api(api_method='nameserver.info', method_params={'domain': INWX_DOMAIN, 'name': f'_acme-challenge.{INWX_SUBDOMAIN}', 'type': 'TXT', 'content': ACME_TOKEN}) # pylint: disable=C0301 + if 'record' not in domain_info_result['resData']: + api_client.logout() + sys.exit(f'No DNS TXT Entry found for _acme-challenge.{INWX_FULLDOMAIN}.') + else: + for row in domain_info_result['resData']['record']: + domain_delete_result = api_client.call_api(api_method='nameserver.deleteRecord', method_params={'id': row['id']}) # pylint: disable=C0301 + if domain_delete_result['code'] == 1000: + if DEBUG: + print(domain_delete_result['msg']) + else: + api_client.logout() + sys.exit(domain_delete_result) + + # With or without successful check, we perform a logout. + api_client.logout() + + # validating return code + if domain_info_result['code'] == 2302: + sys.exit(f"{domain_info_result['msg']}.\nTry nameserver.updateRecord or nameserver.deleteRecord instead") # pylint: disable=C0301 + elif domain_info_result['code'] == 1000: + if DEBUG: + print(domain_info_result['msg']) + sys.exit() + else: + sys.exit(domain_info_result) +else: + sys.exit('Api login error. Code: ' + str(login_result['code']) + ' Message: ' + login_result['msg']) # pylint: disable=C0301