diff --git a/README.md b/README.md index 68af8f0..b6f43e6 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,14 @@ # PowerDNS - Letsencrypt -This role extends the PowerDNS role with another backend to handle Letsencrypt challanges. +This role extends the PowerDNS role with another backend to handle Letsencrypt challenges. ## operation We register a [PowerDNS pipe backend](https://doc.powerdns.com/authoritative/backends/pipe.html) and deploy a python script to serve it. The script is stored at `/usr/local/bin/pdns.py`. This script processes queries matching the regex `^_acme-challenge\\.`. -It can also be called directly with `pdns.py ` to add challanges, for example `pdns.py "_acme-challenge.example.com" "R8aa0mt6cnCVLF6RHsSNxmDBzJffNCK6"` -Challanges older than two days are removed when a new entry is added. +It can also be called directly with `pdns.py add_challenge ` to add challenges, for example `pdns.py add_challenge "_acme-challenge.example.com" "R8aa0mt6cnCVLF6RHsSNxmDBzJffNCK6"` +Challenges older than two days are removed when a new entry is added. +This can be automated using tokens (see `pdns.py --help`) and ssh forced commands. ## parameters diff --git a/tasks/main.yml b/tasks/main.yml index 28f79f9..b6c2c05 100644 --- a/tasks/main.yml +++ b/tasks/main.yml @@ -1,7 +1,14 @@ -- name: install powerdns backends +- name: install powerdns backends and dependencies apt: pkg: - "pdns-backend-pipe" + - "python3-click" + +- name: create letsencrypt user + user: + name: letsencrypt + password: '*' + system: True - name: create folders file: @@ -11,7 +18,14 @@ group: "{{ item.group|d('pdns') }}" mode: "{{ item.mode|d('0755') }}" with_items: - - { "path": "/var/lib/powerdns/letsencrypt/" } + - { "path": "/var/lib/powerdns/letsencrypt/", "owner": "letsencrypt" } + +- name: ensure database permissions + file: + path: "{{ powerdns.letsencrypthandler.dbpath }}" + owner: letsencrypt + group: pdns + mode: 0644 - name: copy powerdns letsencrypt handler template: diff --git a/templates/pdns-letsencrypt.py.j2 b/templates/pdns-letsencrypt.py.j2 index 6ce855e..4811df7 100644 --- a/templates/pdns-letsencrypt.py.j2 +++ b/templates/pdns-letsencrypt.py.j2 @@ -1,8 +1,10 @@ #!/usr/bin/env python3 import sys -from sys import stdin, stdout +from sys import stdin, stdout, stderr +import os import socket import sqlite3 +import click def setupdb(): conn = sqlite3.connect('{{ powerdns.letsencrypthandler.dbpath }}', isolation_level=None) @@ -13,24 +15,58 @@ def setupdb(): timestamp DEFAULT (strftime('%s','now')) ) """) + conn.executescript(""" + CREATE TABLE IF NOT EXISTS tokens ( + token TEXT NOT NULL, + record TEXT NOT NULL, + last_used DEFAULT (strftime('%s','now')), + added DEFAULT (strftime('%s','now')), + UNIQUE(token, record) + ) + """) conn.commit() return conn -def get_challenge(db, path): +def cleanup_db(db): c = db.cursor() - c.execute('SELECT value FROM challenges WHERE q = ?', (path,)) + c.execute("DELETE FROM challenges WHERE timestamp < strftime('%s', datetime('now','-2 day'))") + db.commit() + +def get_challenge(db, record): + c = db.cursor() + c.execute('SELECT value FROM challenges WHERE q = ?', [record]) result = c.fetchall() if result: return result else: - return ['NO DATA - ' + socket.gethostname()] + return [['NO DATA -' + socket.gethostname()]] -def add_challenge(db, path, value): +def add_challenge(db, record, value): c = db.cursor() - c.execute('INSERT INTO challenges (q, value) VALUES(?, ?)', (path,value,)) - c.execute("DELETE FROM challenges WHERE timestamp < strftime('%s', datetime('now','-2 day'))") + c.execute('INSERT INTO challenges (q, value) VALUES(?, ?)', [record, value]) db.commit() +def add_token(db, token, record): + c = db.cursor() + c.execute('INSERT OR IGNORE INTO tokens (token, record, last_used) VALUES(?, ?, NULL)', [token, record]) + db.commit() + +def token_has_access(db, token, record): + c = db.cursor() + c.execute('SELECT * FROM tokens WHERE token = ? AND record = ?', [token, record]) + result = c.fetchall() + if result: + c.execute("UPDATE tokens SET last_used = strftime('%s','now') WHERE token = ? AND record = ?", [token, record]) + db.commit() + return True + else: + return False + +@click.group() +def cli(): + pass + +@cli.command(name="pdns_backend") def main_query(): db = setupdb() data = stdin.readline() @@ -38,6 +74,8 @@ def main_query(): stdout.flush() while True: data = stdin.readline().strip() + if not data: + continue kind, qname, qclass, qtype, id, ip = data.split("\t") if qtype == "SOA": stdout.write("DATA\t" + qname + "\t" + qclass + "\t" + qtype + "\t300\t" + id + "\t") @@ -49,15 +87,50 @@ def main_query(): stdout.write("END\n") stdout.flush() -def main_add_challenge(): +@cli.command(name="add_challenge") +@click.argument('record') +@click.argument('challenge') +def main_add_challenge(record, challenge): db = setupdb() - add_challenge(db ,sys.argv[1], sys.argv[2]) + add_challenge(db, record, challenge) + cleanup_db(db) -def main(): - if len(sys.argv) == 3: - main_add_challenge() +@cli.command(name="add_challenge_with_token") +@click.argument('token') +@click.argument('record') +@click.argument('challenge') +def main_add_challenge_with_token(token, record, challenge): + db = setupdb() + if token_has_access(db, token, record): + add_challenge(db, record, challenge) + cleanup_db(db) + exit(0) else: - main_query() + cleanup_db(db) + print("Token does not have access to this record") + exit(1) + +@cli.command(name="add_challenge_with_token_ssh") +@click.pass_context +def main_add_challenge_with_token_ssh(ctx): + # ssh passes the SSH_ORIGINAL_COMMAND environment variable if you use a forced command + # using this we can support auto renew hooks like this: + # ssh letsencrypt@server + cmd = os.environ.get('SSH_ORIGINAL_COMMAND') + if not cmd: + exit(2) + cmd = cmd.strip().split(' ') + if len(cmd) != 3: + exit(3) + ctx.invoke(main_add_challenge_with_token, token=cmd[0], record=cmd[1], challenge= cmd[2]) + +@cli.command(name="add_token") +@click.argument('token') +@click.argument('record') +def main_add_token(token, record): + db = setupdb() + add_token(db, token, record) + cleanup_db(db) if __name__ == '__main__': - main() + cli() diff --git a/vars/main.yml b/vars/main.yml index 38a1282..8b213e8 100644 --- a/vars/main.yml +++ b/vars/main.yml @@ -3,5 +3,5 @@ powerdns: launch: "pipe:letsencrypt": {} "pipe-letsencrypt-regex": "^_acme-challenge\\." - "pipe-letsencrypt-command": "/usr/local/bin/pdns.py" + "pipe-letsencrypt-command": "/usr/local/bin/pdns.py pdns_backend" "pipe-letsencrypt-abi-version": "1"