Skip to content

Commit

Permalink
Create INWX API ACME Scripts
Browse files Browse the repository at this point in the history
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)
  • Loading branch information
DO1JLR committed Sep 19, 2024
1 parent 51cd039 commit 3df3ff6
Show file tree
Hide file tree
Showing 3 changed files with 261 additions and 0 deletions.
62 changes: 62 additions & 0 deletions dns_scripts/INWX-README.md
Original file line number Diff line number Diff line change
@@ -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="..."
```
93 changes: 93 additions & 0 deletions dns_scripts/dns_add_inwx.py
Original file line number Diff line number Diff line change
@@ -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
106 changes: 106 additions & 0 deletions dns_scripts/dns_del_inwx.py
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 3df3ff6

Please sign in to comment.