Commit 08775fb1 authored by Valentin Samir's avatar Valentin Samir
Browse files

Support de la rotation des KSK. Utilisation de la clef privé pour récupérer les metadata.

Au lieu des commentaires de la clef publique qui ne sont pas forcement là
ou pas forcement avec le bon formatage.
parent c28a4f3c
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf8 -*-
import os import os
import sys import sys
...@@ -7,9 +8,29 @@ import subprocess ...@@ -7,9 +8,29 @@ import subprocess
import argparse import argparse
from functools import total_ordering from functools import total_ordering
BASE = "/etc/bind/keys" BASE = "/etc/bind/keys"
INTERVAL = "3d" # Interval entre 2 opérations sur les clefs dns.
ZSK_VALIDITY = datetime.timedelta(days=30) # Par exemple si vous avec la clef1 d'utilisé,
# clef2 est publié INTERVAL avant la désactivation de clef1.
# clef1 est désactivé INTERVAL après que clef2 est activé,
# clef2 est supprimé INTRERVAL après sa désactivation.
# INTERVAL DOIT être supérieur aux plus long TTL que les enregistrement DS peuvent avoir.
# Cela dépent essentiellement de la configuration de la zone parente et vous n'avez pas forcement
# de controle dessus.
INTERVAL = datetime.timedelta(days=14)
# Durée au bout de laquelle une ZSK est remplacé par une nouvelle ZSK.
# La génération des ZSK et leur activation/désactivation/suppression est géré
# automatiquement tant que routine.py -c est appelé au moins une fois par
# jour.
ZSK_VALIDITY = datetime.timedelta(days=30) # ~1 mois
# Temps au bout duquelle une nouvelle KSK est généré et publié pour la zone
# (et activé après INTERVAL). L'ancienne clef n'est retiré que INTERVAL après que la nouvelle
# clef a été routine.py --ds-seen. Cela demande en général une opération
# manuelle avec le registrar (publier le DS de la nouvelle clef dans la zone parente)
# et routine.py -c affiche un message tant que cela n'a pas été fait.
KSK_VALIDITY = datetime.timedelta(days=366) # ~1 an
def get_zones(zone_names=None): def get_zones(zone_names=None):
l = [] l = []
...@@ -23,7 +44,7 @@ def get_zones(zone_names=None): ...@@ -23,7 +44,7 @@ def get_zones(zone_names=None):
return l return l
def settime(path, flag, date): def settime(path, flag, date):
cmd = ["/usr/sbin/dnssec-settime", "-i", INTERVAL, "-%s" % flag, date, path] cmd = ["/usr/sbin/dnssec-settime", "-i", str(int(INTERVAL.total_seconds())), "-%s" % flag, date, path]
p = subprocess.Popen(cmd, stderr=subprocess.PIPE) p = subprocess.Popen(cmd, stderr=subprocess.PIPE)
err = p.communicate()[1] err = p.communicate()[1]
if p.returncode != 0: if p.returncode != 0:
...@@ -71,13 +92,59 @@ class Zone(object): ...@@ -71,13 +92,59 @@ class Zone(object):
def do_zsk(self): def do_zsk(self):
for zsk in self.ZSK: for zsk in self.ZSK:
if zsk.is_activate: if zsk.is_activate:
inactive = zsk.activate + ZSK_VALIDITY inactive = zsk.activate + INTERVAL
zsk.delete = inactive + ZSK_VALIDITY zsk.delete = inactive + INTERVAL
zsk.inactive = inactive zsk.inactive = inactive
if zsk.is_activate: if zsk.is_activate:
zsk.gen_successor() zsk.gen_successor()
bind_reload() bind_reload()
def do_ksk(self):
ksk = self.KSK[-1]
if ksk.need_renew:
now = datetime.datetime.utcnow()
new_ksk = Key.create("KSK", self.name)
new_ksk.publish = now
new_ksk.activate = (now + INTERVAL)
bind_reload()
active_ksk = [ksk for ksk in self.KSK if ksk.is_publish and ksk.delete is None]
if len(active_ksk)>=2:
sys.stderr.write("New KSK needs DS seen and/or old KSK needs inactivate/remove for zone %s\n" % self.name)
def ds_seen(self, keyid):
old_ksks = []
for ksk in self.KSK:
if ksk.keyid == keyid:
seen_ksk = ksk
break
old_ksks.append(ksk)
else:
sys.stderr.write("Key not found\n")
return
print "Key %s found" % keyid
now = datetime.datetime.utcnow()
for ksk in old_ksks:
print " * program key %s removal" % ksk.keyid
# delete INTERVAL after being inactive
ksk.delete = max(seen_ksk.activate, now + INTERVAL) + INTERVAL
# set inactive in at least INTERVAL
ksk.inactive = max(seen_ksk.activate, now + INTERVAL)
bind_reload()
def remove_deleted(self):
deleted_path = os.path.join(self._path, "deleted")
try:
os.mkdir(deleted_path)
except OSError as error:
if error.errno != 17: # File exists
raise
for key in self.ZSK + self.KSK:
if key.is_delete:
for path in [key._path, key._path_private]:
basename = os.path.basename(path)
new_path = os.path.join(deleted_path, basename)
os.rename(path, new_path)
def ds(self): def ds(self):
for ksk in self.KSK: for ksk in self.KSK:
cmd = ["/usr/sbin/dnssec-dsfromkey", ksk._path] cmd = ["/usr/sbin/dnssec-dsfromkey", ksk._path]
...@@ -97,7 +164,7 @@ class Zone(object): ...@@ -97,7 +164,7 @@ class Zone(object):
self.KSK = [] self.KSK = []
for file in os.listdir(path): for file in os.listdir(path):
file_path = os.path.join(path, file) file_path = os.path.join(path, file)
if os.path.isfile(file_path): if os.path.isfile(file_path) and file_path.endswith(".private"):
try: try:
key = Key(file_path) key = Key(file_path)
if key.type == "ZSK": if key.type == "ZSK":
...@@ -106,8 +173,8 @@ class Zone(object): ...@@ -106,8 +173,8 @@ class Zone(object):
self.KSK.append(key) self.KSK.append(key)
else: else:
raise RuntimeError("impossible") raise RuntimeError("impossible")
except ValueError: except ValueError as error:
pass sys.stderr.write("%s\n" % error)
self.ZSK.sort() self.ZSK.sort()
self.KSK.sort() self.KSK.sort()
if not self.ZSK: if not self.ZSK:
...@@ -126,6 +193,8 @@ class Key(object): ...@@ -126,6 +193,8 @@ class Key(object):
_path = None _path = None
type = None type = None
keyid = None keyid = None
flag = None
zone_name = None
def __str__(self): def __str__(self):
return self._data return self._data
...@@ -147,16 +216,18 @@ class Key(object): ...@@ -147,16 +216,18 @@ class Key(object):
cmd = ["/usr/sbin/dnssec-keygen", "-a", "RSASHA256", "-b", "2048", "-f", "KSK", "-K", path, name] cmd = ["/usr/sbin/dnssec-keygen", "-a", "RSASHA256", "-b", "2048", "-f", "KSK", "-K", path, name]
elif typ == "ZSK": elif typ == "ZSK":
cmd = ["/usr/sbin/dnssec-keygen", "-a", "RSASHA256", "-b", "1024", "-K", path, name] cmd = ["/usr/sbin/dnssec-keygen", "-a", "RSASHA256", "-b", "1024", "-K", path, name]
else:
raise ValueError("typ must be KSK or ZSK")
p = subprocess.Popen(cmd, stdout=subprocess.PIPE) p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
p.wait() p.wait()
if p.returncode != 0: if p.returncode != 0:
raise ValueError("La creation de la clef a echoue") raise ValueError("La creation de la clef a echoue")
keyname = p.communicate()[0].strip() keyname = p.communicate()[0].strip()
bind_chown(path) bind_chown(path)
return cls(os.path.join(path, "%s.key" % keyname)) return cls(os.path.join(path, "%s.private" % keyname))
def gen_successor(self): def gen_successor(self):
cmd = ["/usr/sbin/dnssec-keygen", "-i", INTERVAL, "-S", self._path, "-K", os.path.dirname(self._path)] cmd = ["/usr/sbin/dnssec-keygen", "-i", str(int(INTERVAL.total_seconds())), "-S", self._path, "-K", os.path.dirname(self._path)]
p = subprocess.Popen(cmd, stderr=subprocess.PIPE) p = subprocess.Popen(cmd, stderr=subprocess.PIPE)
err = p.communicate()[1] err = p.communicate()[1]
if p.returncode != 0: if p.returncode != 0:
...@@ -167,11 +238,13 @@ class Key(object): ...@@ -167,11 +238,13 @@ class Key(object):
@property @property
def created(self): def created(self):
return self._date_from_key(self._created) if self._created is not None:
return self._date_from_key(self._created)
@property @property
def publish(self): def publish(self):
return self._date_from_key(self._publish) if self._publish is not None:
return self._date_from_key(self._publish)
@publish.setter @publish.setter
def publish(self, value): def publish(self, value):
date = self._date_to_key(value) date = self._date_to_key(value)
...@@ -183,7 +256,8 @@ class Key(object): ...@@ -183,7 +256,8 @@ class Key(object):
@property @property
def activate(self): def activate(self):
return self._date_from_key(self._activate) if self._activate is not None:
return self._date_from_key(self._activate)
@activate.setter @activate.setter
def activate(self, value): def activate(self, value):
date = self._date_to_key(value) date = self._date_to_key(value)
...@@ -195,7 +269,8 @@ class Key(object): ...@@ -195,7 +269,8 @@ class Key(object):
@property @property
def inactive(self): def inactive(self):
return self._date_from_key(self._inactive) if self._inactive is not None:
return self._date_from_key(self._inactive)
@inactive.setter @inactive.setter
def inactive(self, value): def inactive(self, value):
date = self._date_to_key(value) date = self._date_to_key(value)
...@@ -205,16 +280,10 @@ class Key(object): ...@@ -205,16 +280,10 @@ class Key(object):
with open(self._path, 'r') as f: with open(self._path, 'r') as f:
self._data = f.read() self._data = f.read()
@property
def is_activate(self):
return self.activate <= datetime.datetime.now()
@property
def is_inactive(self):
return self.inactive <= datetime.datetime.now()
@property @property
def delete(self): def delete(self):
return self._date_from_key(self._delete) if self._delete:
return self._date_from_key(self._delete)
@delete.setter @delete.setter
def delete(self, value): def delete(self, value):
date = self._date_to_key(value) date = self._date_to_key(value)
...@@ -224,34 +293,95 @@ class Key(object): ...@@ -224,34 +293,95 @@ class Key(object):
with open(self._path, 'r') as f: with open(self._path, 'r') as f:
self._data = f.read() self._data = f.read()
@property
def is_publish(self):
return self.publish is not None and self.publish <= datetime.datetime.utcnow()
@property
def is_activate(self):
return self.activate is not None and self.activate <= datetime.datetime.utcnow()
@property
def is_inactive(self):
return self.inactive is not None and self.inactive <= datetime.datetime.utcnow()
@property
def is_delete(self):
return self.delete is not None and self.delete <= datetime.datetime.utcnow()
@property
def need_renew(self):
if self.type == "KSK":
return (self.activate + KSK_VALIDITY) <= (datetime.datetime.utcnow() + INTERVAL)
elif self.type == "ZSK":
return (self.activate + ZSK_VALIDITY) <= (datetime.datetime.utcnow() + INTERVAL)
else:
raise RuntimeError("impossible")
def __init__(self, path): def __init__(self, path):
with open(path, 'r') as f: if not path.endswith(".private"):
raise ValueError("%s n'est pas une clef valide" % path)
if not os.path.isfile(path):
raise ValueError("%s n'existe pas" % path)
ppath = "%s.key" % path[:-8]
if not os.path.isfile(ppath):
raise ValueError("la clef publique (%s) de %s n'existe pas" % (ppath, path))
with open(ppath, 'r') as f:
self._data = f.read() self._data = f.read()
if "This is a zone-signing key" in self._data: with open(path, 'r') as f:
private_data = f.read()
for line in self._data.split("\n"):
if line.startswith(";") or not line:
continue
line = line.split(";", 1)[0].strip()
line = line.split()
if len(line) < 7:
raise ValueError("La clef publique %s devrait avoir au moins 7 champs: %r" % (ppath, line))
if not line[0].endswith('.'):
raise ValueError("La clef publique %s devrait commencer par le fqdn (finissant par un .) de la zone" % ppath)
self.zone_name = line[0][:-1]
try:
self.flag = int(line[3])
except ValueError:
raise ValueError("Le flag %s de la clef publique %s devrait être un entier" % (line[3], ppath))
if self.flag == 256:
self.type = "ZSK" self.type = "ZSK"
elif "This is a key-signing key" in self._data: elif self.flag == 257:
self.type = "KSK" self.type = "KSK"
else: else:
raise ValueError("%s n'est pas une clef valide" % path) raise ValueError("%s n'est pas une clef valide: flag %s inconnu" % (ppath, self.flag))
self._path = path self._path = ppath
lines = self._data.split("\n") self._path_private = path
self. keyid = lines[0].split(',')[1].split()[-1] keyid = path.split('.')[-2].split('+')[-1]
for line in lines: try:
if line.startswith("; Created: "): self.keyid = int(keyid)
self._created = line[11:11+14] except ValueError:
elif line.startswith("; Publish: "): raise ValueError("Le keyid %s de la clef %s devrait être un entier" % (keyid, path))
self._publish = line[11:11+14] for line in private_data.split("\n"):
elif line.startswith("; Activate: "): if line.startswith("Created:"):
self._activate = line[12:12+14] self._created = line[8:].strip()
elif line.startswith("; Inactive: "): self._date_from_key(self._created)
self._inactive = line[12:12+14] elif line.startswith("Publish:"):
elif line.startswith("; Delete: "): self._publish = line[8:].strip()
self._delete = line[10:10+14] self._date_from_key(self._publish)
elif line.startswith("Activate:"):
self._activate = line[9:].strip()
self._date_from_key(self._activate)
elif line.startswith("Inactive:"):
self._inactive = line[9:].strip()
self._date_from_key(self._inactive)
elif line.startswith("Delete:"):
self._delete = line[7:].strip()
self._date_from_key(self._delete)
if self.created is None:
raise ValueError("La clef %s doit au moins avoir le champs Created de définit" % path)
def __lt__(self, y): def __lt__(self, y):
if not isinstance(y, Key): if not isinstance(y, Key):
raise ValueError("can only compare two Keys") raise ValueError("can only compare two Keys")
return self.activate < y.activate if self.activate is not None and y.activate is not None:
return self.activate < y.activate
elif self.publish is not None and y.publish is not None:
return self.publish < y.publish
else:
return self.created < y.created
def __eq__(self, y): def __eq__(self, y):
return isinstance(y, Key) and y._path == self._path return isinstance(y, Key) and y._path == self._path
...@@ -264,22 +394,31 @@ if __name__ == '__main__': ...@@ -264,22 +394,31 @@ if __name__ == '__main__':
parser.add_argument('--cron', '-c', action='store_true', help='Perform maintenance for each supplied zone or for all zones if no zone supplied') parser.add_argument('--cron', '-c', action='store_true', help='Perform maintenance for each supplied zone or for all zones if no zone supplied')
parser.add_argument('-ds', action='store_true', help='Show DS for each supplied zone or for all zones if no zone supplied') parser.add_argument('-ds', action='store_true', help='Show DS for each supplied zone or for all zones if no zone supplied')
parser.add_argument('-key', action='store_true', help='Show DNSKEY for each zone supplied zone or for all zones if no zone supplied') parser.add_argument('-key', action='store_true', help='Show DNSKEY for each zone supplied zone or for all zones if no zone supplied')
parser.add_argument('--ds-seen', metavar='KEYID', type=int, help='To call with the ID of a new KSK published in the parent zone. Programs old KSK removal')
args = parser.parse_args() args = parser.parse_args()
zones = args.zone zones = args.zone
if args.make: if args.make:
for zone in zones: for zone in zones:
Zone.create(zone) Zone.create(zone)
zones = get_zones(zones if zones else None) zones = get_zones(zones if zones else None)
if args.ds_seen:
if len(zones) != 1:
sys.stderr.write("Please specify exactly ONE zone name\n")
sys.exit(1)
for zone in zones:
zone.ds_seen(args.ds_seen)
if args.cron: if args.cron:
for zone in zones: for zone in zones:
zone.do_zsk() zone.do_zsk()
zone.do_ksk()
zone.remove_deleted()
if args.ds: if args.ds:
for zone in zones: for zone in zones:
zone.ds() zone.ds()
if args.key: if args.key:
for zone in zones: for zone in zones:
zone.key() zone.key()
if not any([args.make, args.cron, args.ds, args.key]): if not any([args.make, args.cron, args.ds, args.key, args.ds_seen]):
parser.print_help() parser.print_help()
except ValueError as error: except ValueError as error:
sys.stderr.write("%s\n" % error) sys.stderr.write("%s\n" % error)
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment