add token support to script to support ssh forced commands with tokens

This commit is contained in:
nd 2020-06-27 04:54:17 +02:00
parent cd1e83b833
commit 7dcf71c3ab
No known key found for this signature in database
GPG key ID: 21B5CD4DEE3670E9
4 changed files with 108 additions and 20 deletions

View file

@ -1,13 +1,14 @@
# PowerDNS - Letsencrypt # 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 ## operation
We register a [PowerDNS pipe backend](https://doc.powerdns.com/authoritative/backends/pipe.html) and deploy a python script to serve it. 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\\.`. 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 <dns entry> <value>` to add challanges, for example `pdns.py "_acme-challenge.example.com" "R8aa0mt6cnCVLF6RHsSNxmDBzJffNCK6"` It can also be called directly with `pdns.py add_challenge <dns entry> <value>` to add challenges, for example `pdns.py add_challenge "_acme-challenge.example.com" "R8aa0mt6cnCVLF6RHsSNxmDBzJffNCK6"`
Challanges older than two days are removed when a new entry is added. 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 ## parameters

View file

@ -1,7 +1,14 @@
- name: install powerdns backends - name: install powerdns backends and dependencies
apt: apt:
pkg: pkg:
- "pdns-backend-pipe" - "pdns-backend-pipe"
- "python3-click"
- name: create letsencrypt user
user:
name: letsencrypt
password: '*'
system: True
- name: create folders - name: create folders
file: file:
@ -11,7 +18,14 @@
group: "{{ item.group|d('pdns') }}" group: "{{ item.group|d('pdns') }}"
mode: "{{ item.mode|d('0755') }}" mode: "{{ item.mode|d('0755') }}"
with_items: 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 - name: copy powerdns letsencrypt handler
template: template:

View file

@ -1,8 +1,10 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import sys import sys
from sys import stdin, stdout from sys import stdin, stdout, stderr
import os
import socket import socket
import sqlite3 import sqlite3
import click
def setupdb(): def setupdb():
conn = sqlite3.connect('{{ powerdns.letsencrypthandler.dbpath }}', isolation_level=None) conn = sqlite3.connect('{{ powerdns.letsencrypthandler.dbpath }}', isolation_level=None)
@ -13,24 +15,58 @@ def setupdb():
timestamp DEFAULT (strftime('%s','now')) 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() conn.commit()
return conn return conn
def get_challenge(db, path): def cleanup_db(db):
c = db.cursor() 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() result = c.fetchall()
if result: if result:
return result return result
else: 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 = db.cursor()
c.execute('INSERT INTO challenges (q, value) VALUES(?, ?)', (path,value,)) c.execute('INSERT INTO challenges (q, value) VALUES(?, ?)', [record, value])
c.execute("DELETE FROM challenges WHERE timestamp < strftime('%s', datetime('now','-2 day'))")
db.commit() 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(): def main_query():
db = setupdb() db = setupdb()
data = stdin.readline() data = stdin.readline()
@ -38,6 +74,8 @@ def main_query():
stdout.flush() stdout.flush()
while True: while True:
data = stdin.readline().strip() data = stdin.readline().strip()
if not data:
continue
kind, qname, qclass, qtype, id, ip = data.split("\t") kind, qname, qclass, qtype, id, ip = data.split("\t")
if qtype == "SOA": if qtype == "SOA":
stdout.write("DATA\t" + qname + "\t" + qclass + "\t" + qtype + "\t300\t" + id + "\t") 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.write("END\n")
stdout.flush() 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() db = setupdb()
add_challenge(db ,sys.argv[1], sys.argv[2]) add_challenge(db, record, challenge)
cleanup_db(db)
def main(): @cli.command(name="add_challenge_with_token")
if len(sys.argv) == 3: @click.argument('token')
main_add_challenge() @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: 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 <token> <record> <challenge>
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__': if __name__ == '__main__':
main() cli()

View file

@ -3,5 +3,5 @@ powerdns:
launch: launch:
"pipe:letsencrypt": {} "pipe:letsencrypt": {}
"pipe-letsencrypt-regex": "^_acme-challenge\\." "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" "pipe-letsencrypt-abi-version": "1"