add token support to script to support ssh forced commands with tokens
This commit is contained in:
parent
cd1e83b833
commit
7dcf71c3ab
4 changed files with 108 additions and 20 deletions
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue