diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 0000000..3e68739 --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,83 @@ +#!/usr/bin/env bash + +# Git pre-commit hook to check staged Python files for formatting issues with +# yapf. +# +# INSTALLING: Copy this script into `.git/hooks/pre-commit`, and mark it as +# executable. +# +# This requires that yapf is installed and runnable in the environment running +# the pre-commit hook. +# +# When running, this first checks for unstaged changes to staged files, and if +# there are any, it will exit with an error. Files with unstaged changes will be +# printed. +# +# If all staged files have no unstaged changes, it will run yapf against them, +# leaving the formatting changes unstaged. Changed files will be printed. +# +# BUGS: This does not leave staged changes alone when used with the -a flag to +# git commit, due to the fact that git stages ALL unstaged files when that flag +# is used. + +# Find all staged Python files, and exit early if there aren't any. +PYTHON_FILES=() +while IFS=$'\n' read -r line; do PYTHON_FILES+=("$line"); done \ + < <(git diff --name-only --cached --diff-filter=AM | grep --color=never '.py$') +if [ ${#PYTHON_FILES[@]} -eq 0 ]; then + exit 0 +fi + +########## PIP VERSION ############# +# Verify that yapf is installed; if not, warn and exit. +if ! command -v yapf >/dev/null; then + echo 'yapf not on path; can not format. Please install yapf:' + echo ' pip install yapf' + exit 2 +fi +######### END PIP VERSION ########## + +########## PIPENV VERSION ########## +# if ! pipenv run yapf --version 2>/dev/null 2>&1; then +# echo 'yapf not on path; can not format. Please install yapf:' +# echo ' pipenv install yapf' +# exit 2 +# fi +###### END PIPENV VERSION ########## + + +# Check for unstaged changes to files in the index. +CHANGED_FILES=() +while IFS=$'\n' read -r line; do CHANGED_FILES+=("$line"); done \ + < <(git diff --name-only "${PYTHON_FILES[@]}") +if [ ${#CHANGED_FILES[@]} -gt 0 ]; then + echo 'You have unstaged changes to some files in your commit; skipping ' + echo 'auto-format. Please stage, stash, or revert these changes. You may ' + echo 'find `git stash -k` helpful here.' + echo 'Files with unstaged changes:' "${CHANGED_FILES[@]}" + exit 1 +fi + +# Format all staged files, then exit with an error code if any have uncommitted +# changes. +echo 'Formatting staged Python files . . .' + +########## PIP VERSION ############# +yapf -i -r "${PYTHON_FILES[@]}" +######### END PIP VERSION ########## + +########## PIPENV VERSION ########## +# pipenv run yapf -i -r "${PYTHON_FILES[@]}" +###### END PIPENV VERSION ########## + + +CHANGED_FILES=() +while IFS=$'\n' read -r line; do CHANGED_FILES+=("$line"); done \ + < <(git diff --name-only "${PYTHON_FILES[@]}") +if [ ${#CHANGED_FILES[@]} -gt 0 ]; then + echo 'Reformatted staged files. Please review and stage the changes.' + echo 'Files updated: ' "${CHANGED_FILES[@]}" + exit 1 +else + exit 0 +fi diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..34aada9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +snikket.conf +*/__pycache__ diff --git a/Dockerfile b/Dockerfile index 5759607..45ecb66 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,16 +1,24 @@ +FROM coredns/coredns:latest AS coredns +# this image is only used for copying coredns binary +# ca-certificates are added by apt below + FROM debian:bullseye-slim +COPY --from=coredns /coredns /usr/sbin/coredns ARG BUILD_SERIES=dev ARG BUILD_ID=0 VOLUME ["/snikket"] +EXPOSE 53 53/udp + ENTRYPOINT ["/usr/bin/tini"] CMD ["/bin/sh", "/entrypoint.sh"] RUN apt-get update \ && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ - certbot tini anacron jq \ + certbot tini anacron jq ca-certificates python3 dnsutils \ + && update-ca-certificates \ && rm -rf /var/lib/apt/lists/* \ && apt-get autoremove -y \ && rm -rf /var/cache/* \ @@ -23,3 +31,4 @@ ADD certbot.cron /etc/cron.daily/certbot ADD sendmail /usr/sbin/sendmail RUN chmod 555 /etc/cron.daily/certbot RUN useradd -md /snikket/letsencrypt letsencrypt +COPY certbot-coredns /certbot-coredns diff --git a/certbot-coredns/.style.yapf b/certbot-coredns/.style.yapf new file mode 100644 index 0000000..108d85c --- /dev/null +++ b/certbot-coredns/.style.yapf @@ -0,0 +1,404 @@ +[style] +# Align closing bracket with visual indentation. +align_closing_bracket_with_visual_indent=True + +# Allow dictionary keys to exist on multiple lines. For example: +# +# x = { +# ('this is the first element of a tuple', +# 'this is the second element of a tuple'): +# value, +# } +allow_multiline_dictionary_keys=False + +# Allow lambdas to be formatted on more than one line. +allow_multiline_lambdas=False + +# Allow splitting before a default / named assignment in an argument list. +allow_split_before_default_or_named_assigns=True + +# Allow splits before the dictionary value. +allow_split_before_dict_value=False + +# Let spacing indicate operator precedence. For example: +# +# a = 1 * 2 + 3 / 4 +# b = 1 / 2 - 3 * 4 +# c = (1 + 2) * (3 - 4) +# d = (1 - 2) / (3 + 4) +# e = 1 * 2 - 3 +# f = 1 + 2 + 3 + 4 +# +# will be formatted as follows to indicate precedence: +# +# a = 1*2 + 3/4 +# b = 1/2 - 3*4 +# c = (1+2) * (3-4) +# d = (1-2) / (3+4) +# e = 1*2 - 3 +# f = 1 + 2 + 3 + 4 +# +arithmetic_precedence_indication=False + +# Number of blank lines surrounding top-level function and class +# definitions. +blank_lines_around_top_level_definition=2 + +# Number of blank lines between top-level imports and variable +# definitions. +blank_lines_between_top_level_imports_and_variables=1 + +# Insert a blank line before a class-level docstring. +blank_line_before_class_docstring=False + +# Insert a blank line before a module docstring. +blank_line_before_module_docstring=False + +# Insert a blank line before a 'def' or 'class' immediately nested +# within another 'def' or 'class'. For example: +# +# class Foo: +# # <------ this blank line +# def method(): +# ... +blank_line_before_nested_class_or_def=False + +# Do not split consecutive brackets. Only relevant when +# dedent_closing_brackets is set. For example: +# +# call_func_that_takes_a_dict( +# { +# 'key1': 'value1', +# 'key2': 'value2', +# } +# ) +# +# would reformat to: +# +# call_func_that_takes_a_dict({ +# 'key1': 'value1', +# 'key2': 'value2', +# }) +coalesce_brackets=False + +# The column limit. +column_limit=80 + +# The style for continuation alignment. Possible values are: +# +# - SPACE: Use spaces for continuation alignment. This is default behavior. +# - FIXED: Use fixed number (CONTINUATION_INDENT_WIDTH) of columns +# (ie: CONTINUATION_INDENT_WIDTH/INDENT_WIDTH tabs or +# CONTINUATION_INDENT_WIDTH spaces) for continuation alignment. +# - VALIGN-RIGHT: Vertically align continuation lines to multiple of +# INDENT_WIDTH columns. Slightly right (one tab or a few spaces) if +# cannot vertically align continuation lines with indent characters. +continuation_align_style=SPACE + +# Indent width used for line continuations. +continuation_indent_width=4 + +# Put closing brackets on a separate line, dedented, if the bracketed +# expression can't fit in a single line. Applies to all kinds of brackets, +# including function definitions and calls. For example: +# +# config = { +# 'key1': 'value1', +# 'key2': 'value2', +# } # <--- this bracket is dedented and on a separate line +# +# time_series = self.remote_client.query_entity_counters( +# entity='dev3246.region1', +# key='dns.query_latency_tcp', +# transform=Transformation.AVERAGE(window=timedelta(seconds=60)), +# start_ts=now()-timedelta(days=3), +# end_ts=now(), +# ) # <--- this bracket is dedented and on a separate line +dedent_closing_brackets=True + +# Disable the heuristic which places each list element on a separate line +# if the list is comma-terminated. +disable_ending_comma_heuristic=True + +# Place each dictionary entry onto its own line. +each_dict_entry_on_separate_line=True + +# Require multiline dictionary even if it would normally fit on one line. +# For example: +# +# config = { +# 'key1': 'value1' +# } +force_multiline_dict=True + +# The regex for an i18n comment. The presence of this comment stops +# reformatting of that line, because the comments are required to be +# next to the string they translate. +i18n_comment=#\..* + +# The i18n function call names. The presence of this function stops +# reformattting on that line, because the string it has cannot be moved +# away from the i18n comment. +i18n_function_call=N_, _ + +# Indent blank lines. +indent_blank_lines=False + +# Put closing brackets on a separate line, indented, if the bracketed +# expression can't fit in a single line. Applies to all kinds of brackets, +# including function definitions and calls. For example: +# +# config = { +# 'key1': 'value1', +# 'key2': 'value2', +# } # <--- this bracket is indented and on a separate line +# +# time_series = self.remote_client.query_entity_counters( +# entity='dev3246.region1', +# key='dns.query_latency_tcp', +# transform=Transformation.AVERAGE(window=timedelta(seconds=60)), +# start_ts=now()-timedelta(days=3), +# end_ts=now(), +# ) # <--- this bracket is indented and on a separate line +indent_closing_brackets=False + +# Indent the dictionary value if it cannot fit on the same line as the +# dictionary key. For example: +# +# config = { +# 'key1': +# 'value1', +# 'key2': value1 + +# value2, +# } +indent_dictionary_value=False + +# The number of columns to use for indentation. +indent_width=4 + +# Join short lines into one line. E.g., single line 'if' statements. +join_multiple_lines=False + +# Do not include spaces around selected binary operators. For example: +# +# 1 + 2 * 3 - 4 / 5 +# +# will be formatted as follows when configured with "*,/": +# +# 1 + 2*3 - 4/5 +no_spaces_around_selected_binary_operators= + +# Use spaces around default or named assigns. +spaces_around_default_or_named_assign=True + +# Adds a space after the opening '{' and before the ending '}' dict +# delimiters. +# +# {1: 2} +# +# will be formatted as: +# +# { 1: 2 } +spaces_around_dict_delimiters=True + +# Adds a space after the opening '[' and before the ending ']' list +# delimiters. +# +# [1, 2] +# +# will be formatted as: +# +# [ 1, 2 ] +spaces_around_list_delimiters=True + +# Use spaces around the power operator. +spaces_around_power_operator=False + +# Use spaces around the subscript / slice operator. For example: +# +# my_list[1 : 10 : 2] +spaces_around_subscript_colon=True + +# Adds a space after the opening '(' and before the ending ')' tuple +# delimiters. +# +# (1, 2, 3) +# +# will be formatted as: +# +# ( 1, 2, 3 ) +spaces_around_tuple_delimiters=True + +# The number of spaces required before a trailing comment. +# This can be a single value (representing the number of spaces +# before each trailing comment) or list of values (representing +# alignment column values; trailing comments within a block will +# be aligned to the first column value that is greater than the maximum +# line length within the block). For example: +# +# With spaces_before_comment=5: +# +# 1 + 1 # Adding values +# +# will be formatted as: +# +# 1 + 1 # Adding values <-- 5 spaces between the end of the +# # statement and comment +# +# With spaces_before_comment=15, 20: +# +# 1 + 1 # Adding values +# two + two # More adding +# +# longer_statement # This is a longer statement +# short # This is a shorter statement +# +# a_very_long_statement_that_extends_beyond_the_final_column # Comment +# short # This is a shorter statement +# +# will be formatted as: +# +# 1 + 1 # Adding values <-- end of line comments in block +# # aligned to col 15 +# two + two # More adding +# +# longer_statement # This is a longer statement <-- end of line +# # comments in block aligned to col 20 +# short # This is a shorter statement +# +# a_very_long_statement_that_extends_beyond_the_final_column # Comment <-- the end of line comments are aligned based on the line length +# short # This is a shorter statement +# +spaces_before_comment=2 + +# Insert a space between the ending comma and closing bracket of a list, +# etc. +space_between_ending_comma_and_closing_bracket=True + +# Use spaces inside brackets, braces, and parentheses. For example: +# +# method_call( 1 ) +# my_dict[ 3 ][ 1 ][ get_index( *args, **kwargs ) ] +# my_set = { 1, 2, 3 } +space_inside_brackets=True + +# Split before arguments +split_all_comma_separated_values=True + +# Split before arguments, but do not split all subexpressions recursively +# (unless needed). +split_all_top_level_comma_separated_values=False + +# Split before arguments if the argument list is terminated by a +# comma. +split_arguments_when_comma_terminated=True + +# Set to True to prefer splitting before '+', '-', '*', '/', '//', or '@' +# rather than after. +split_before_arithmetic_operator=False + +# Set to True to prefer splitting before '&', '|' or '^' rather than +# after. +split_before_bitwise_operator=False + +# Split before the closing bracket if a list or dict literal doesn't fit on +# a single line. +split_before_closing_bracket=True + +# Split before a dictionary or set generator (comp_for). For example, note +# the split before the 'for': +# +# foo = { +# variable: 'Hello world, have a nice day!' +# for variable in bar if variable != 42 +# } +split_before_dict_set_generator=True + +# Split before the '.' if we need to split a longer expression: +# +# foo = ('This is a really long string: {}, {}, {}, {}'.format(a, b, c, d)) +# +# would reformat to something like: +# +# foo = ('This is a really long string: {}, {}, {}, {}' +# .format(a, b, c, d)) +split_before_dot=True + +# Split after the opening paren which surrounds an expression if it doesn't +# fit on a single line. +split_before_expression_after_opening_paren=False + +# If an argument / parameter list is going to be split, then split before +# the first argument. +split_before_first_argument=False + +# Set to True to prefer splitting before 'and' or 'or' rather than +# after. +split_before_logical_operator=False + +# Split named assignments onto individual lines. +split_before_named_assigns=True + +# Set to True to split list comprehensions and generators that have +# non-trivial expressions and multiple clauses before each of these +# clauses. For example: +# +# result = [ +# a_long_var + 100 for a_long_var in xrange(1000) +# if a_long_var % 10] +# +# would reformat to something like: +# +# result = [ +# a_long_var + 100 +# for a_long_var in xrange(1000) +# if a_long_var % 10] +split_complex_comprehension=True + +# The penalty for splitting right after the opening bracket. +split_penalty_after_opening_bracket=300 + +# The penalty for splitting the line after a unary operator. +split_penalty_after_unary_operator=10000 + +# The penalty of splitting the line around the '+', '-', '*', '/', '//', +# ``%``, and '@' operators. +split_penalty_arithmetic_operator=300 + +# The penalty for splitting right before an if expression. +split_penalty_before_if_expr=0 + +# The penalty of splitting the line around the '&', '|', and '^' +# operators. +split_penalty_bitwise_operator=300 + +# The penalty for splitting a list comprehension or generator +# expression. +split_penalty_comprehension=2100 + +# The penalty for characters over the column limit. +split_penalty_excess_character=7000 + +# The penalty incurred by adding a line split to the logical line. The +# more line splits added the higher the penalty. +split_penalty_for_added_line_split=30 + +# The penalty of splitting a list of "import as" names. For example: +# +# from a_very_long_or_indented_module_name_yada_yad import (long_argument_1, +# long_argument_2, +# long_argument_3) +# +# would reformat to something like: +# +# from a_very_long_or_indented_module_name_yada_yad import ( +# long_argument_1, long_argument_2, long_argument_3) +split_penalty_import_names=0 + +# The penalty of splitting the line around the 'and' and 'or' +# operators. +split_penalty_logical_operator=300 + +# Use the Tab character for indentation. +use_tabs=False + diff --git a/certbot-coredns/__init__.py b/certbot-coredns/__init__.py new file mode 100755 index 0000000..e5a0d9b --- /dev/null +++ b/certbot-coredns/__init__.py @@ -0,0 +1 @@ +#!/usr/bin/env python3 diff --git a/certbot-coredns/auth.py b/certbot-coredns/auth.py new file mode 100755 index 0000000..2210149 --- /dev/null +++ b/certbot-coredns/auth.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 + +import os +import subprocess +import time + +from common import write_or_update_master_file + + +def main(): + # make sure certificates are being fetched for + # the correct Snikket domain + domain = os.environ[ "CERTBOT_DOMAIN" ] + print( "[AuthHook] Obtaining certificates for {}".format( domain ) ) + if domain.startswith( "*." ): + domain = domain[ 2 : ] + assert ( domain == os.environ[ "SNIKKET_TWEAK_XMPP_DOMAIN" ] ) + token = os.environ[ "CERTBOT_VALIDATION" ] + print( "[AuthHook] Writing TXT record {}".format( token ) ) + write_or_update_master_file( token ) + print( "[AuthHook] Waiting for 25s to propagate TXT record" ) + # below section does not seem to work well + # that is, it gives old data + # perhaps this needs to be done @1.1.1.1 + # but that is not good for privacy + # so just do a sleep instead + # fetched_tokens = [] + # while len(fetched_tokens) < 1: + # result = subprocess.run(["dig", "-t", "txt", + # "cert.snikket.{}".format(domain), + # "+short"], + # stdout=subprocess.PIPE) + # fetched_tokens = result.stdout.decode("utf-8"). \ + # replace('"', '').strip().split("\n") + # if token not in fetched_tokens: + # print( + # "[AuthHook] Got different record {}".format( + # fetched_tokens + # ) + # ) + # fetched_tokens = [] + time.sleep( 25 ) # how long Certbot example waits + print( "[AuthHook] Done waiting" ) + + +if __name__ == "__main__": + main() diff --git a/certbot-coredns/common.py b/certbot-coredns/common.py new file mode 100755 index 0000000..492fda2 --- /dev/null +++ b/certbot-coredns/common.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 + +import os +import time +from textwrap import dedent + + +# converts email address to an SOA format, that is, +# @ is replaced by . +# dots in the username are replaced by \. +# for example, +# hostmaster@example.com becomes hostmaster.example.com +# host.master@example.com becomes host\.master.example.com +def email_to_rname( email ): + username, domain = email.split( "@", 1 ) + username = username.replace( ".", "\\." ) # escape . in username + return ".".join( ( username, domain ) ) + + +# Creates or updates the RFC-1035 compliant file at master_file_path +# note that CoreDNS only reloads the file if serial of SOA changes +# this is why we rewrite the entire file with new timestamp +# even when we are updating / adding just a single record +def write_or_update_master_file( token = "", delete = False ): + master_file_content = """\ + ; Zone: SNIKKET.{domain}. + + $ORIGIN SNIKKET.{domain}. + $TTL 0 + + ; SOA Record + @ IN SOA NS-SNIKKET.{domain}. {email}. ( + {serial} ;serial + 300 ;refresh + 600 ;retry + 600 ;expire + 0 ;minimum ttl + ) + + ; NS Records + @ IN NS NS-SNIKKET.{domain}. + + ; TXT Records + {last_line} + """ + domain = os.environ[ "SNIKKET_TWEAK_XMPP_DOMAIN" ] + master_file_path = "/snikket/coredns/db.snikket.{}".format( domain ) + if os.path.exists( master_file_path ): + if delete: + os.remove( master_file_path ) + with open( master_file_path, "r" ) as f: + existing_master_file_content = f.read() + existing_last_line = existing_master_file_content.strip(). \ + split("\n")[-1] + else: + existing_last_line = "" + existing_last_line = existing_last_line if "cert" in \ + existing_last_line else "" + if existing_last_line: # already had a TXT record + assert ( token ) # so we must be updating + last_line = \ + "{}\n cert IN TXT {}\n".format( + existing_last_line, + token + ) + elif token: # add a new token + last_line = "cert IN TXT {}\n".format( token ) + else: # initial set up without TXT + last_line = "" + with open( master_file_path, "w" ) as f: + f.write( + dedent( + master_file_content.format( + domain = domain.upper(), + serial = int( time.time() ), + email = email_to_rname( + os.environ[ "SNIKKET_ADMIN_EMAIL" ] + ).upper(), + last_line = last_line + ) + ) + ) diff --git a/certbot-coredns/post.py b/certbot-coredns/post.py new file mode 100755 index 0000000..a56f6ad --- /dev/null +++ b/certbot-coredns/post.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 + +import os +import signal + + +def main(): + if os.path.exists( "/snikket/coredns/pid" ): + print( "[PostHook] Killing CoreDNS" ) + with open( "/snikket/coredns/pid" ) as f: + pid = f.read() + os.kill( int( pid ), signal.SIGTERM ) + else: + print( + "[PostHook] Did not find CoreDNS PID, likely due to error in PreHook" + ) + print( "[PostHook] Removing config files" ) + os.remove( "/snikket/coredns/pid" ) + os.remove( "/snikket/coredns/Corefile" ) + os.remove( + "/snikket/coredns/db.snikket.{}".format( + os.environ[ "SNIKKET_TWEAK_XMPP_DOMAIN" ] + ) + ) + + +if __name__ == "__main__": + main() diff --git a/certbot-coredns/pre.py b/certbot-coredns/pre.py new file mode 100755 index 0000000..1109f3b --- /dev/null +++ b/certbot-coredns/pre.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python3 + +# validate the NS and CNAME records +# A/AAAA records for the name server are not validated +# start the DNS server with only basic records +# will not contain any TXT records until auth.py is run + +import os +import subprocess +import sys +import time +from textwrap import dedent + +from common import write_or_update_master_file + + +def write_core_file( domain ): + core_file = """ + . {{ + file /snikket/coredns/db.snikket.{domain} snikket.{domain} {{ + reload 1s + }} + }} + """ + f = open( "/snikket/coredns/Corefile", "w" ) + f.write( dedent( core_file.format( domain = domain, ) ) ) + f.close() + + +def validate_dns_record( domain, record_name, expected_value, hint = "" ): + print( + "[PreHook] Validating '{}' record for '{}': " + "expecting value of '{}'".format( + record_name.upper(), + domain, + expected_value + ) + ) + result = subprocess.run( + [ "dig", + domain, + record_name.lower(), + "+short" ], + stdout = subprocess.PIPE + ) + value = result.stdout.decode( "utf-8" ).strip() + if value != expected_value: + print( + "[PreHook] '{}' record for '{}' needs to be " + "set to '{}' but got '{}'".format( + record_name.upper(), + domain, + expected_value, + value + ) + ) + if hint: + print( hint ) + sys.exit( 1 ) + print( + "[PreHook] Validated '{}' record for '{}'".format( + record_name.upper(), + domain, + ) + ) + + +def validate_cname_record( domain ): + validate_dns_record( + "_acme-challenge.{}".format( domain ), + "cname", + "cert.snikket.{}.".format( domain ) + ) + + +def validate_ns_record( domain ): + validate_dns_record( + "snikket.{}".format( domain ), + "ns", + "ns-snikket.{}.".format( domain ), + "[PreHook] Is the A/AAAA record of " + "ns-snikket.{} correctly set?".format( domain ) + ) + + +def validate_a_or_aaaa_record( domain ): + if "SNIKKET_EXTERNAL_IP" in os.environ: + validate_dns_record( + "ns-snikket.{}".format( domain ), + "aaaa" if ":" in os.environ[ "SNIKKET_EXTERNAL_IP" ] else "a", + os.environ[ "SNIKKET_EXTERNAL_IP" ] + ) + else: + print( + "[PreHook] Skipping A/AAAA check as " + "SNIKKET_EXTERNAL_IP is not available" + ) + + +def main(): + domain = os.environ[ "SNIKKET_TWEAK_XMPP_DOMAIN" ] + print( "[PreHook] Running for domain {}".format( domain ) ) + validate_a_or_aaaa_record( domain ) + validate_cname_record( domain ) + write_or_update_master_file( delete = True ) + write_core_file( domain ) + print( "[PreHook] Starting DNS server" ) + with open( "/dev/null", "w" ) as devnull: + process = subprocess.Popen( + [ "/usr/sbin/coredns", + "--conf", + "/snikket/coredns/Corefile" ], + stdin = None, + stdout = devnull, + stderr = devnull, + close_fds = True + ) + print( "[PreHook] Writing PID" ) + with open( "/snikket/coredns/pid", "w" ) as f: + f.write( str( process.pid ) ) + print( "[PreHook] Sleeping for 25s to ensure everything is up" ) + time.sleep( 25 ) + # now that the CoreDNS server is running + # check for NS value + validate_ns_record( domain ) + + +if __name__ == "__main__": + main() diff --git a/certbot.cron b/certbot.cron index 260cada..41e7a78 100644 --- a/certbot.cron +++ b/certbot.cron @@ -1,18 +1,35 @@ #!/bin/sh if test -f /var/log/letsencrypt/letsencrypt.log; then - # Preserve previous log until next run - mv /var/log/letsencrypt/letsencrypt.log /var/log/letsencrypt/letsencrypt.log.old; + # Preserve previous log until next run + mv /var/log/letsencrypt/letsencrypt.log /var/log/letsencrypt/letsencrypt.log.old; fi -su letsencrypt -- -c "certbot certonly -n --webroot --webroot-path /var/www \ - --cert-path /etc/ssl/certbot \ - --keep $SNIKKET_CERTBOT_OPTIONS \ - --agree-tos --email \"$SNIKKET_ADMIN_EMAIL\" --expand \ - --config-dir /snikket/letsencrypt \ - --domain \"$SNIKKET_DOMAIN\" --domain \"share.$SNIKKET_DOMAIN\" \ - --domain \"groups.$SNIKKET_DOMAIN\" - " +if [ $SNIKKET_DNS_CHALLENGE = 0 ]; then + su letsencrypt -- -c "certbot certonly -n --webroot --webroot-path /var/www \ + --cert-path /etc/ssl/certbot \ + --keep $SNIKKET_CERTBOT_OPTIONS \ + --agree-tos --email \"$SNIKKET_ADMIN_EMAIL\" --expand \ + --allow-subset-of-names \ + --config-dir /snikket/letsencrypt \ + --domain \"$SNIKKET_DOMAIN\" --domain \"share.$SNIKKET_DOMAIN\" \ + --domain \"groups.$SNIKKET_DOMAIN\" + " +else + echo "Using DNS challenge" + su letsencrypt -- -c "certbot certonly -n --manual \ + --preferred-challenges dns \ + --pre-hook /certbot-coredns/pre.py \ + --manual-auth-hook /certbot-coredns/auth.py \ + --post-hook /certbot-coredns/post.py \ + --cert-path /etc/ssl/certbot \ + --keep $SNIKKET_CERTBOT_OPTIONS \ + --agree-tos --email \"$SNIKKET_ADMIN_EMAIL\" --expand \ + --config-dir /snikket/letsencrypt \ + --domain \"*.$SNIKKET_TWEAK_XMPP_DOMAIN\" \ + --domain \"$SNIKKET_TWEAK_XMPP_DOMAIN\" + " +fi sed -n '/^{/,/^}/p' /var/log/letsencrypt/letsencrypt.log \ | jq -r '(select(.status=="invalid").challenges | .[].error?.detail ), select(.detail).detail' \ diff --git a/entrypoint.sh b/entrypoint.sh index 8fde9a1..0d91c11 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -18,4 +18,25 @@ install -o letsencrypt -g letsencrypt -m 755 -d /var/www/.well-known/acme-challe chown -R letsencrypt:letsencrypt /snikket/letsencrypt +export SNIKKET_DNS_CHALLENGE=${SNIKKET_DNS_CHALLENGE:-0} +if [ $SNIKKET_DNS_CHALLENGE = 0 ]; then + if [ -z $SNIKKET_TWEAK_XMPP_DOMAIN ]; then + : + else + echo "Using DNS challenge as SNIKKET_TWEAK_XMPP_DOMAIN is set" + export SNIKKET_DNS_CHALLENGE=1 + fi +else + if [ -z $SNIKKET_TWEAK_XMPP_DOMAIN ]; then + export SNIKKET_TWEAK_XMPP_DOMAIN=$SNIKKET_DOMAIN + fi +fi +if [ $SNIKKET_DNS_CHALLENGE = 1 ]; then + echo "Using DNS challenge" + if ! test -d /snikket/coredns; then + install -o letsencrypt -g letsencrypt -m 750 -d /snikket/coredns; + fi + chown -R letsencrypt:letsencrypt /snikket/coredns +fi + exec /bin/sh -c "/usr/sbin/anacron -d -n && sleep 3600"