=== modified file 'DBUS-API'
--- DBUS-API 2011-10-02 19:18:24 +0000
+++ DBUS-API 2011-11-26 23:08:17 +0000
@@ -112,7 +112,7 @@
Disable().
f) The date and time this client will be disabled, as an RFC 3339
- string, or an empty string if this has not happened.
+ string, or an empty string if this is not scheduled.
g) The date and time of the last approval request, as an RFC 3339
string, or an empty string if this has not happened.
@@ -152,6 +152,9 @@
*** Rejected(s: Reason)
This client was not given its secret for a specified Reason.
+*** NewRequest(s: IP address)
+ A client at IP adress has asked for its password.
+
* Copyright
Copyright © 2010-2011 Teddy Hogeborn
=== modified file 'Makefile'
--- Makefile 2011-10-22 00:46:35 +0000
+++ Makefile 2011-11-26 22:22:20 +0000
@@ -26,12 +26,16 @@
version=1.4.1
SED=sed
+USER=$(firstword $(subst :, ,$(shell getent passwd _mandos || getent passwd nobody || echo 65534)))
+GROUP=$(firstword $(subst :, ,$(shell getent group _mandos || getent group nobody || echo 65534)))
+
## Use these settings for a traditional /usr/local install
# PREFIX=$(DESTDIR)/usr/local
# CONFDIR=$(DESTDIR)/etc/mandos
# KEYDIR=$(DESTDIR)/etc/mandos/keys
# MANDIR=$(PREFIX)/man
# INITRAMFSTOOLS=$(DESTDIR)/etc/initramfs-tools
+# STATEDIR=$(DESTDIR)/var/lib/mandos
##
## These settings are for a package-type install
@@ -40,6 +44,7 @@
KEYDIR=$(DESTDIR)/etc/keys/mandos
MANDIR=$(PREFIX)/share/man
INITRAMFSTOOLS=$(DESTDIR)/usr/share/initramfs-tools
+STATEDIR=$(DESTDIR)/var/lib/mandos
##
GNUTLS_CFLAGS=$(shell pkg-config --cflags-only-I gnutls)
@@ -230,7 +235,7 @@
distclean: clean
mostlyclean: clean
maintainer-clean: clean
- -rm --force --recursive keydir confdir
+ -rm --force --recursive keydir confdir statedir
check: all
./mandos --check
@@ -260,7 +265,8 @@
# Run the server with a local config
run-server: confdir/mandos.conf confdir/clients.conf
- ./mandos --debug --no-dbus --configdir=confdir $(SERVERARGS)
+ ./mandos --debug --no-dbus --configdir=confdir \
+ --statedir=statedir $(SERVERARGS)
# Used by run-server
confdir/mandos.conf: mandos.conf
@@ -271,6 +277,8 @@
install --mode=u=rw $< $@
# Add a client password
./mandos-keygen --dir keydir --password >> $@
+statedir:
+ install --directory statedir
install: install-server install-client-nokey
@@ -281,6 +289,8 @@
install-server: doc
install --directory $(CONFDIR)
+ install --directory --mode=u=rwx --owner=$(USER) \
+ --group=$(GROUP) $(STATEDIR)
install --mode=u=rwx,go=rx mandos $(PREFIX)/sbin/mandos
install --mode=u=rwx,go=rx --target-directory=$(PREFIX)/sbin \
mandos-ctl
=== modified file 'TODO'
--- TODO 2011-11-10 11:08:50 +0000
+++ TODO 2011-12-21 00:29:33 +0000
@@ -40,11 +40,10 @@
** TODO [#B] Use openat()
* mandos (server)
+** TODO Document why we ignore sigint
** TODO [#B] Log level :BUGS:
-** TODO [#A] Move approval to Client from ClientDBus
+** TODO [#A] Save state to new file and move instead of overwrite
-*** TODO /etc/mandos/clients.d/*.conf
- Watch this directory and add/remove/update clients?
** TODO [#C] config for TXT record
** TODO Log level option
syslogger.setLevel(logging.WARNING)
=== modified file 'clients.conf'
--- clients.conf 2011-09-19 09:42:55 +0000
+++ clients.conf 2011-11-26 23:08:17 +0000
@@ -30,6 +30,9 @@
# How long one approval will last.
;approval_duration = 1s
+# Whether this client is enabled by default
+;enabled = True
+
;####
;# Example client
=== modified file 'debian/control'
--- debian/control 2011-11-10 11:08:50 +0000
+++ debian/control 2011-12-21 00:29:33 +0000
@@ -17,7 +17,8 @@
Architecture: all
Depends: ${misc:Depends}, python (>=2.6), python-gnutls, python-dbus,
python-avahi, python-gobject, avahi-daemon, adduser,
- python-urwid, python (>=2.7) | python-argparse, python-crypto
+ python-urwid, python (>=2.7) | python-argparse,
+ python-gnupginterface
Recommends: fping
Description: server giving encrypted passwords to Mandos clients
This is the server part of the Mandos system, which allows
=== modified file 'debian/mandos.dirs'
--- debian/mandos.dirs 2010-09-15 17:33:14 +0000
+++ debian/mandos.dirs 2011-11-26 22:22:20 +0000
@@ -4,3 +4,4 @@
etc/default
etc/dbus-1/system.d
usr/sbin
+var/lib/mandos
=== modified file 'debian/mandos.postinst'
--- debian/mandos.postinst 2011-10-10 20:29:58 +0000
+++ debian/mandos.postinst 2011-11-26 22:22:20 +0000
@@ -35,11 +35,12 @@
--disabled-password --gecos "Mandos password system" \
_mandos
fi
+ chown _mandos:_mandos /var/lib/mandos
;;
-
+
abort-upgrade|abort-deconfigure|abort-remove)
;;
-
+
*)
echo "$0 called with unknown argument '$1'" 1>&2
exit 1
=== modified file 'mandos'
--- mandos 2011-12-03 01:04:54 +0000
+++ mandos 2011-12-21 00:29:33 +0000
@@ -63,7 +63,8 @@
import cPickle as pickle
import multiprocessing
import types
-import hashlib
+import binascii
+import tempfile
import dbus
import dbus.service
@@ -74,7 +75,7 @@
import ctypes.util
import xml.dom.minidom
import inspect
-import Crypto.Cipher.AES
+import GnuPGInterface
try:
SO_BINDTODEVICE = socket.SO_BINDTODEVICE
@@ -84,26 +85,126 @@
except ImportError:
SO_BINDTODEVICE = None
-
version = "1.4.1"
+stored_state_file = "clients.pickle"
logger = logging.getLogger()
-stored_state_path = "/var/lib/mandos/clients.pickle"
-
syslogger = (logging.handlers.SysLogHandler
(facility = logging.handlers.SysLogHandler.LOG_DAEMON,
address = str("/dev/log")))
-syslogger.setFormatter(logging.Formatter
- ('Mandos [%(process)d]: %(levelname)s:'
- ' %(message)s'))
-logger.addHandler(syslogger)
-
-console = logging.StreamHandler()
-console.setFormatter(logging.Formatter('%(asctime)s %(name)s'
- ' [%(process)d]:'
- ' %(levelname)s:'
- ' %(message)s'))
-logger.addHandler(console)
+
+try:
+ if_nametoindex = (ctypes.cdll.LoadLibrary
+ (ctypes.util.find_library("c"))
+ .if_nametoindex)
+except (OSError, AttributeError):
+ def if_nametoindex(interface):
+ "Get an interface index the hard way, i.e. using fcntl()"
+ SIOCGIFINDEX = 0x8933 # From /usr/include/linux/sockios.h
+ with contextlib.closing(socket.socket()) as s:
+ ifreq = fcntl.ioctl(s, SIOCGIFINDEX,
+ struct.pack(str("16s16x"),
+ interface))
+ interface_index = struct.unpack(str("I"),
+ ifreq[16:20])[0]
+ return interface_index
+
+
+def initlogger(level=logging.WARNING):
+ """init logger and add loglevel"""
+
+ syslogger.setFormatter(logging.Formatter
+ ('Mandos [%(process)d]: %(levelname)s:'
+ ' %(message)s'))
+ logger.addHandler(syslogger)
+
+ console = logging.StreamHandler()
+ console.setFormatter(logging.Formatter('%(asctime)s %(name)s'
+ ' [%(process)d]:'
+ ' %(levelname)s:'
+ ' %(message)s'))
+ logger.addHandler(console)
+ logger.setLevel(level)
+
+
+class PGPError(Exception):
+ """Exception if encryption/decryption fails"""
+ pass
+
+
+class PGPEngine(object):
+ """A simple class for OpenPGP symmetric encryption & decryption"""
+ def __init__(self):
+ self.gnupg = GnuPGInterface.GnuPG()
+ self.tempdir = tempfile.mkdtemp(prefix="mandos-")
+ self.gnupg = GnuPGInterface.GnuPG()
+ self.gnupg.options.meta_interactive = False
+ self.gnupg.options.homedir = self.tempdir
+ self.gnupg.options.extra_args.extend(['--force-mdc',
+ '--quiet'])
+
+ def __enter__(self):
+ return self
+
+ def __exit__ (self, exc_type, exc_value, traceback):
+ self._cleanup()
+ return False
+
+ def __del__(self):
+ self._cleanup()
+
+ def _cleanup(self):
+ if self.tempdir is not None:
+ # Delete contents of tempdir
+ for root, dirs, files in os.walk(self.tempdir,
+ topdown = False):
+ for filename in files:
+ os.remove(os.path.join(root, filename))
+ for dirname in dirs:
+ os.rmdir(os.path.join(root, dirname))
+ # Remove tempdir
+ os.rmdir(self.tempdir)
+ self.tempdir = None
+
+ def password_encode(self, password):
+ # Passphrase can not be empty and can not contain newlines or
+ # NUL bytes. So we prefix it and hex encode it.
+ return b"mandos" + binascii.hexlify(password)
+
+ def encrypt(self, data, password):
+ self.gnupg.passphrase = self.password_encode(password)
+ with open(os.devnull) as devnull:
+ try:
+ proc = self.gnupg.run(['--symmetric'],
+ create_fhs=['stdin', 'stdout'],
+ attach_fhs={'stderr': devnull})
+ with contextlib.closing(proc.handles['stdin']) as f:
+ f.write(data)
+ with contextlib.closing(proc.handles['stdout']) as f:
+ ciphertext = f.read()
+ proc.wait()
+ except IOError as e:
+ raise PGPError(e)
+ self.gnupg.passphrase = None
+ return ciphertext
+
+ def decrypt(self, data, password):
+ self.gnupg.passphrase = self.password_encode(password)
+ with open(os.devnull) as devnull:
+ try:
+ proc = self.gnupg.run(['--decrypt'],
+ create_fhs=['stdin', 'stdout'],
+ attach_fhs={'stderr': devnull})
+ with contextlib.closing(proc.handles['stdin'] ) as f:
+ f.write(data)
+ with contextlib.closing(proc.handles['stdout']) as f:
+ decrypted_plaintext = f.read()
+ proc.wait()
+ except IOError as e:
+ raise PGPError(e)
+ self.gnupg.passphrase = None
+ return decrypted_plaintext
+
class AvahiError(Exception):
@@ -226,7 +327,7 @@
try:
self.group.Free()
except (dbus.exceptions.UnknownMethodException,
- dbus.exceptions.DBusException) as e:
+ dbus.exceptions.DBusException):
pass
self.group = None
self.remove()
@@ -276,7 +377,7 @@
% self.name))
return ret
-def _timedelta_to_milliseconds(td):
+def timedelta_to_milliseconds(td):
"Convert a datetime.timedelta() to milliseconds"
return ((td.days * 24 * 60 * 60 * 1000)
+ (td.seconds * 1000)
@@ -286,7 +387,7 @@
"""A representation of a client host served by this server.
Attributes:
- _approved: bool(); 'None' if not yet approved/disapproved
+ approved: bool(); 'None' if not yet approved/disapproved
approval_delay: datetime.timedelta(); Time to wait for approval
approval_duration: datetime.timedelta(); Duration of one approval
checker: subprocess.Popen(); a running checker process used
@@ -311,9 +412,9 @@
last_approval_request: datetime.datetime(); (UTC) or None
last_checked_ok: datetime.datetime(); (UTC) or None
last_checker_status: integer between 0 and 255 reflecting exit
- status of last checker. -1 reflect crashed
+ status of last checker. -1 reflects crashed
checker, or None.
- last_enabled: datetime.datetime(); (UTC)
+ last_enabled: datetime.datetime(); (UTC) or None
name: string; from the config file, used in log messages and
D-Bus identifiers
secret: bytestring; sent verbatim (over TLS) to client
@@ -329,81 +430,115 @@
"created", "enabled", "fingerprint",
"host", "interval", "last_checked_ok",
"last_enabled", "name", "timeout")
+ client_defaults = { "timeout": "5m",
+ "extended_timeout": "15m",
+ "interval": "2m",
+ "checker": "fping -q -- %%(host)s",
+ "host": "",
+ "approval_delay": "0s",
+ "approval_duration": "1s",
+ "approved_by_default": "True",
+ "enabled": "True",
+ }
def timeout_milliseconds(self):
"Return the 'timeout' attribute in milliseconds"
- return _timedelta_to_milliseconds(self.timeout)
+ return timedelta_to_milliseconds(self.timeout)
def extended_timeout_milliseconds(self):
"Return the 'extended_timeout' attribute in milliseconds"
- return _timedelta_to_milliseconds(self.extended_timeout)
+ return timedelta_to_milliseconds(self.extended_timeout)
def interval_milliseconds(self):
"Return the 'interval' attribute in milliseconds"
- return _timedelta_to_milliseconds(self.interval)
+ return timedelta_to_milliseconds(self.interval)
def approval_delay_milliseconds(self):
- return _timedelta_to_milliseconds(self.approval_delay)
-
- def __init__(self, name = None, config=None):
+ return timedelta_to_milliseconds(self.approval_delay)
+
+ @staticmethod
+ def config_parser(config):
+ """ Construct a new dict of client settings of this form:
+ { client_name: {setting_name: value, ...}, ...}
+ with exceptions for any special settings as defined above"""
+ settings = {}
+ for client_name in config.sections():
+ section = dict(config.items(client_name))
+ client = settings[client_name] = {}
+
+ client["host"] = section["host"]
+ # Reformat values from string types to Python types
+ client["approved_by_default"] = config.getboolean(
+ client_name, "approved_by_default")
+ client["enabled"] = config.getboolean(client_name, "enabled")
+
+ client["fingerprint"] = (section["fingerprint"].upper()
+ .replace(" ", ""))
+ if "secret" in section:
+ client["secret"] = section["secret"].decode("base64")
+ elif "secfile" in section:
+ with open(os.path.expanduser(os.path.expandvars
+ (section["secfile"])),
+ "rb") as secfile:
+ client["secret"] = secfile.read()
+ else:
+ raise TypeError("No secret or secfile for section %s"
+ % section)
+ client["timeout"] = string_to_delta(section["timeout"])
+ client["extended_timeout"] = string_to_delta(
+ section["extended_timeout"])
+ client["interval"] = string_to_delta(section["interval"])
+ client["approval_delay"] = string_to_delta(
+ section["approval_delay"])
+ client["approval_duration"] = string_to_delta(
+ section["approval_duration"])
+ client["checker_command"] = section["checker"]
+ client["last_approval_request"] = None
+ client["last_checked_ok"] = None
+ client["last_checker_status"] = None
+ if client["enabled"]:
+ client["last_enabled"] = datetime.datetime.utcnow()
+ client["expires"] = (datetime.datetime.utcnow()
+ + client["timeout"])
+ else:
+ client["last_enabled"] = None
+ client["expires"] = None
+
+ return settings
+
+
+ def __init__(self, settings, name = None):
"""Note: the 'checker' key in 'config' sets the
'checker_command' attribute and *not* the 'checker'
attribute."""
self.name = name
- if config is None:
- config = {}
+ # adding all client settings
+ for setting, value in settings.iteritems():
+ setattr(self, setting, value)
+
logger.debug("Creating client %r", self.name)
# Uppercase and remove spaces from fingerprint for later
# comparison purposes with return value from the fingerprint()
# function
- self.fingerprint = (config["fingerprint"].upper()
- .replace(" ", ""))
logger.debug(" Fingerprint: %s", self.fingerprint)
- if "secret" in config:
- self.secret = config["secret"].decode("base64")
- elif "secfile" in config:
- with open(os.path.expanduser(os.path.expandvars
- (config["secfile"])),
- "rb") as secfile:
- self.secret = secfile.read()
- else:
- raise TypeError("No secret or secfile for client %s"
- % self.name)
- self.host = config.get("host", "")
- self.created = datetime.datetime.utcnow()
- self.enabled = True
- self.last_approval_request = None
- self.last_enabled = datetime.datetime.utcnow()
- self.last_checked_ok = None
- self.last_checker_status = None
- self.timeout = string_to_delta(config["timeout"])
- self.extended_timeout = string_to_delta(config
- ["extended_timeout"])
- self.interval = string_to_delta(config["interval"])
+ self.created = settings.get("created", datetime.datetime.utcnow())
+
+ # attributes specific for this server instance
self.checker = None
self.checker_initiator_tag = None
self.disable_initiator_tag = None
- self.expires = datetime.datetime.utcnow() + self.timeout
self.checker_callback_tag = None
- self.checker_command = config["checker"]
self.current_checker_command = None
- self._approved = None
- self.approved_by_default = config.get("approved_by_default",
- True)
+ self.approved = None
self.approvals_pending = 0
- self.approval_delay = string_to_delta(
- config["approval_delay"])
- self.approval_duration = string_to_delta(
- config["approval_duration"])
self.changedstate = (multiprocessing_manager
.Condition(multiprocessing_manager
.Lock()))
- self.client_structure = [attr for attr
- in self.__dict__.iterkeys()
+ self.client_structure = [attr for attr in
+ self.__dict__.iterkeys()
if not attr.startswith("_")]
self.client_structure.append("client_structure")
-
-
+
for name, t in inspect.getmembers(type(self),
lambda obj:
isinstance(obj,
@@ -449,7 +584,7 @@
def __del__(self):
self.disable()
-
+
def init_checker(self):
# Schedule a new checker to be started an 'interval' from now,
# and every interval from then on.
@@ -462,14 +597,13 @@
self.disable))
# Also start a new checker *right now*.
self.start_checker()
-
-
+
def checker_callback(self, pid, condition, command):
"""The checker has completed, so take appropriate actions."""
self.checker_callback_tag = None
self.checker = None
if os.WIFEXITED(condition):
- self.last_checker_status = os.WEXITSTATUS(condition)
+ self.last_checker_status = os.WEXITSTATUS(condition)
if self.last_checker_status == 0:
logger.info("Checker for %(name)s succeeded",
vars(self))
@@ -495,7 +629,7 @@
gobject.source_remove(self.disable_initiator_tag)
if getattr(self, "enabled", False):
self.disable_initiator_tag = (gobject.timeout_add
- (_timedelta_to_milliseconds
+ (timedelta_to_milliseconds
(timeout), self.disable))
self.expires = datetime.datetime.utcnow() + timeout
@@ -595,56 +729,6 @@
raise
self.checker = None
- # Encrypts a client secret and stores it in a varible
- # encrypted_secret
- def encrypt_secret(self, key):
- # Encryption-key need to be of a specific size, so we hash
- # supplied key
- hasheng = hashlib.sha256()
- hasheng.update(key)
- encryptionkey = hasheng.digest()
-
- # Create validation hash so we know at decryption if it was
- # sucessful
- hasheng = hashlib.sha256()
- hasheng.update(self.secret)
- validationhash = hasheng.digest()
-
- # Encrypt secret
- iv = os.urandom(Crypto.Cipher.AES.block_size)
- ciphereng = Crypto.Cipher.AES.new(encryptionkey,
- Crypto.Cipher.AES.MODE_CFB, iv)
- ciphertext = ciphereng.encrypt(validationhash+self.secret)
- self.encrypted_secret = (ciphertext, iv)
-
- # Decrypt a encrypted client secret
- def decrypt_secret(self, key):
- # Decryption-key need to be of a specific size, so we hash
- # supplied key
- hasheng = hashlib.sha256()
- hasheng.update(key)
- encryptionkey = hasheng.digest()
-
- # Decrypt encrypted secret
- ciphertext, iv = self.encrypted_secret
- ciphereng = Crypto.Cipher.AES.new(encryptionkey,
- Crypto.Cipher.AES.MODE_CFB, iv)
- plain = ciphereng.decrypt(ciphertext)
-
- # Validate decrypted secret to know if it was succesful
- hasheng = hashlib.sha256()
- validationhash = plain[:hasheng.digest_size]
- secret = plain[hasheng.digest_size:]
- hasheng.update(secret)
-
- # If validation fails, we use key as new secret. Otherwise, we
- # use the decrypted secret
- if hasheng.digest() == validationhash:
- self.secret = secret
- else:
- self.secret = key
- del self.encrypted_secret
-
def dbus_service_property(dbus_interface, signature="v",
access="readwrite", byte_arrays=False):
@@ -768,7 +852,7 @@
Note: Will not include properties with access="write".
"""
- all = {}
+ properties = {}
for name, prop in self._get_all_dbus_properties():
if (interface_name
and interface_name != prop._dbus_interface):
@@ -779,11 +863,11 @@
continue
value = prop()
if not hasattr(value, "variant_level"):
- all[name] = value
+ properties[name] = value
continue
- all[name] = type(value)(value, variant_level=
- value.variant_level+1)
- return dbus.Dictionary(all, signature="sv")
+ properties[name] = type(value)(value, variant_level=
+ value.variant_level+1)
+ return dbus.Dictionary(properties, signature="sv")
@dbus.service.method(dbus.INTROSPECTABLE_IFACE,
out_signature="s",
@@ -840,6 +924,7 @@
return dbus.String(dt.isoformat(),
variant_level=variant_level)
+
class AlternateDBusNamesMetaclass(DBusObjectWithProperties
.__metaclass__):
"""Applied to an empty subclass of a D-Bus object, this metaclass
@@ -937,6 +1022,7 @@
attribute.func_closure)))
return type.__new__(mcs, name, bases, attr)
+
class ClientDBus(Client, DBusObjectWithProperties):
"""A Client class using D-Bus
@@ -952,11 +1038,10 @@
def __init__(self, bus = None, *args, **kwargs):
self.bus = bus
- self._approvals_pending = 0
Client.__init__(self, *args, **kwargs)
- self.add_to_dbus()
-
- def add_to_dbus(self):
+ self._approvals_pending = 0
+
+ self._approvals_pending = 0
# Only now, when this client is initialized, can it show up on
# the D-Bus
client_object_name = unicode(self.name).translate(
@@ -972,7 +1057,7 @@
variant_level=1):
""" Modify a variable so that it's a property which announces
its changes to DBus.
-
+
transform_fun: Function that takes a value and a variant_level
and transforms it to a D-Bus type.
dbus_name: D-Bus name of the variable
@@ -1015,21 +1100,21 @@
approval_delay = notifychangeproperty(dbus.UInt64,
"ApprovalDelay",
type_func =
- _timedelta_to_milliseconds)
+ timedelta_to_milliseconds)
approval_duration = notifychangeproperty(
dbus.UInt64, "ApprovalDuration",
- type_func = _timedelta_to_milliseconds)
+ type_func = timedelta_to_milliseconds)
host = notifychangeproperty(dbus.String, "Host")
timeout = notifychangeproperty(dbus.UInt64, "Timeout",
type_func =
- _timedelta_to_milliseconds)
+ timedelta_to_milliseconds)
extended_timeout = notifychangeproperty(
dbus.UInt64, "ExtendedTimeout",
- type_func = _timedelta_to_milliseconds)
+ type_func = timedelta_to_milliseconds)
interval = notifychangeproperty(dbus.UInt64,
"Interval",
type_func =
- _timedelta_to_milliseconds)
+ timedelta_to_milliseconds)
checker_command = notifychangeproperty(dbus.String, "Checker")
del notifychangeproperty
@@ -1077,13 +1162,13 @@
return r
def _reset_approved(self):
- self._approved = None
+ self.approved = None
return False
def approve(self, value=True):
self.send_changedstate()
- self._approved = value
- gobject.timeout_add(_timedelta_to_milliseconds
+ self.approved = value
+ gobject.timeout_add(timedelta_to_milliseconds
(self.approval_duration),
self._reset_approved)
@@ -1132,6 +1217,14 @@
"D-Bus signal"
return self.need_approval()
+ # NeRwequest - signal
+ @dbus.service.signal(_interface, signature="s")
+ def NewRequest(self, ip):
+ """D-Bus signal
+ Is sent after a client request a password.
+ """
+ pass
+
## Methods
# Approve - method
@@ -1195,7 +1288,7 @@
access="readwrite")
def ApprovalDuration_dbus_property(self, value=None):
if value is None: # get
- return dbus.UInt64(_timedelta_to_milliseconds(
+ return dbus.UInt64(timedelta_to_milliseconds(
self.approval_duration))
self.approval_duration = datetime.timedelta(0, 0, 0, value)
@@ -1215,12 +1308,12 @@
def Host_dbus_property(self, value=None):
if value is None: # get
return dbus.String(self.host)
- self.host = value
+ self.host = unicode(value)
# Created - property
@dbus_service_property(_interface, signature="s", access="read")
def Created_dbus_property(self):
- return dbus.String(datetime_to_dbus(self.created))
+ return datetime_to_dbus(self.created)
# LastEnabled - property
@dbus_service_property(_interface, signature="s", access="read")
@@ -1270,11 +1363,11 @@
gobject.source_remove(self.disable_initiator_tag)
self.disable_initiator_tag = None
self.expires = None
- time_to_die = _timedelta_to_milliseconds((self
- .last_checked_ok
- + self.timeout)
- - datetime.datetime
- .utcnow())
+ time_to_die = timedelta_to_milliseconds((self
+ .last_checked_ok
+ + self.timeout)
+ - datetime.datetime
+ .utcnow())
if time_to_die <= 0:
# The timeout has passed
self.disable()
@@ -1302,11 +1395,12 @@
self.interval = datetime.timedelta(0, 0, 0, value)
if getattr(self, "checker_initiator_tag", None) is None:
return
- # Reschedule checker run
- gobject.source_remove(self.checker_initiator_tag)
- self.checker_initiator_tag = (gobject.timeout_add
- (value, self.start_checker))
- self.start_checker() # Start one now, too
+ if self.enabled:
+ # Reschedule checker run
+ gobject.source_remove(self.checker_initiator_tag)
+ self.checker_initiator_tag = (gobject.timeout_add
+ (value, self.start_checker))
+ self.start_checker() # Start one now, too
# Checker - property
@dbus_service_property(_interface, signature="s",
@@ -1314,7 +1408,7 @@
def Checker_dbus_property(self, value=None):
if value is None: # get
return dbus.String(self.checker_command)
- self.checker_command = value
+ self.checker_command = unicode(value)
# CheckerRunning - property
@dbus_service_property(_interface, signature="b",
@@ -1349,7 +1443,7 @@
raise KeyError()
def __getattribute__(self, name):
- if(name == '_pipe'):
+ if name == '_pipe':
return super(ProxyClient, self).__getattribute__(name)
self._pipe.send(('getattr', name))
data = self._pipe.recv()
@@ -1362,13 +1456,15 @@
return func
def __setattr__(self, name, value):
- if(name == '_pipe'):
+ if name == '_pipe':
return super(ProxyClient, self).__setattr__(name, value)
self._pipe.send(('setattr', name, value))
+
class ClientDBusTransitional(ClientDBus):
__metaclass__ = AlternateDBusNamesMetaclass
+
class ClientHandler(socketserver.BaseRequestHandler, object):
"""A class to handle client connections.
@@ -1442,6 +1538,10 @@
except KeyError:
return
+ if self.server.use_dbus:
+ # Emit D-Bus signal
+ client.NewRequest(str(self.client_address))
+
if client.approval_delay:
delay = client.approval_delay
client.approvals_pending += 1
@@ -1456,10 +1556,10 @@
client.Rejected("Disabled")
return
- if client._approved or not client.approval_delay:
+ if client.approved or not client.approval_delay:
#We are approved or approval is disabled
break
- elif client._approved is None:
+ elif client.approved is None:
logger.info("Client %s needs approval",
client.name)
if self.server.use_dbus:
@@ -1479,7 +1579,7 @@
time = datetime.datetime.now()
client.changedstate.acquire()
(client.changedstate.wait
- (float(client._timedelta_to_milliseconds(delay)
+ (float(client.timedelta_to_milliseconds(delay)
/ 1000)))
client.changedstate.release()
time2 = datetime.datetime.now()
@@ -1584,7 +1684,7 @@
# Convert the buffer to a Python bytestring
fpr = ctypes.string_at(buf, buf_len.value)
# Convert the bytestring to hexadecimal notation
- hex_fpr = ''.join("%02X" % ord(char) for char in fpr)
+ hex_fpr = binascii.hexlify(fpr).upper()
return hex_fpr
@@ -1593,7 +1693,7 @@
def sub_process_main(self, request, address):
try:
self.finish_request(request, address)
- except:
+ except Exception:
self.handle_error(request, address)
self.close_request(request)
@@ -1847,30 +1947,6 @@
return timevalue
-def if_nametoindex(interface):
- """Call the C function if_nametoindex(), or equivalent
-
- Note: This function cannot accept a unicode string."""
- global if_nametoindex
- try:
- if_nametoindex = (ctypes.cdll.LoadLibrary
- (ctypes.util.find_library("c"))
- .if_nametoindex)
- except (OSError, AttributeError):
- logger.warning("Doing if_nametoindex the hard way")
- def if_nametoindex(interface):
- "Get an interface index the hard way, i.e. using fcntl()"
- SIOCGIFINDEX = 0x8933 # From /usr/include/linux/sockios.h
- with contextlib.closing(socket.socket()) as s:
- ifreq = fcntl.ioctl(s, SIOCGIFINDEX,
- struct.pack(str("16s16x"),
- interface))
- interface_index = struct.unpack(str("I"),
- ifreq[16:20])[0]
- return interface_index
- return if_nametoindex(interface)
-
-
def daemon(nochdir = False, noclose = False):
"""See daemon(3). Standard BSD Unix function.
@@ -1932,10 +2008,11 @@
parser.add_argument("--no-ipv6", action="store_false",
dest="use_ipv6", help="Do not use IPv6")
parser.add_argument("--no-restore", action="store_false",
- dest="restore",
- help="Do not restore stored state",
- default=True)
-
+ dest="restore", help="Do not restore stored"
+ " state")
+ parser.add_argument("--statedir", metavar="DIR",
+ help="Directory to save/restore state in")
+
options = parser.parse_args()
if options.check:
@@ -1954,6 +2031,8 @@
"use_dbus": "True",
"use_ipv6": "True",
"debuglevel": "",
+ "restore": "True",
+ "statedir": "/var/lib/mandos"
}
# Parse config file for server-global settings
@@ -1976,7 +2055,8 @@
# options, if set.
for option in ("interface", "address", "port", "debug",
"priority", "servicename", "configdir",
- "use_dbus", "use_ipv6", "debuglevel", "restore"):
+ "use_dbus", "use_ipv6", "debuglevel", "restore",
+ "statedir"):
value = getattr(options, option)
if value is not None:
server_settings[option] = value
@@ -1994,6 +2074,17 @@
debuglevel = server_settings["debuglevel"]
use_dbus = server_settings["use_dbus"]
use_ipv6 = server_settings["use_ipv6"]
+ stored_state_path = os.path.join(server_settings["statedir"],
+ stored_state_file)
+
+ if debug:
+ initlogger(logging.DEBUG)
+ else:
+ if not debuglevel:
+ initlogger()
+ else:
+ level = getattr(logging, debuglevel.upper())
+ initlogger(level)
if server_settings["servicename"] != "Mandos":
syslogger.setFormatter(logging.Formatter
@@ -2002,15 +2093,7 @@
% server_settings["servicename"]))
# Parse config file with clients
- client_defaults = { "timeout": "5m",
- "extended_timeout": "15m",
- "interval": "2m",
- "checker": "fping -q -- %%(host)s",
- "host": "",
- "approval_delay": "0s",
- "approval_duration": "1s",
- }
- client_config = configparser.SafeConfigParser(client_defaults)
+ client_config = configparser.SafeConfigParser(Client.client_defaults)
client_config.read(os.path.join(server_settings["configdir"],
"clients.conf"))
@@ -2054,14 +2137,7 @@
if error[0] != errno.EPERM:
raise error
- if not debug and not debuglevel:
- logger.setLevel(logging.WARNING)
- if debuglevel:
- level = getattr(logging, debuglevel.upper())
- logger.setLevel(level)
-
if debug:
- logger.setLevel(logging.DEBUG)
# Enable all possible GnuTLS debugging
# "Use a log level over 10 to enable all debugging options."
@@ -2124,103 +2200,89 @@
client_class = functools.partial(ClientDBusTransitional,
bus = bus)
- special_settings = {
- # Some settings need to be accessd by special methods;
- # booleans need .getboolean(), etc. Here is a list of them:
- "approved_by_default":
- lambda section:
- client_config.getboolean(section, "approved_by_default"),
- }
- # Construct a new dict of client settings of this form:
- # { client_name: {setting_name: value, ...}, ...}
- # with exceptions for any special settings as defined above
- client_settings = dict((clientname,
- dict((setting,
- (value if
- setting not in special_settings
- else special_settings[setting]
- (clientname)))
- for setting, value
- in client_config.items(clientname)))
- for clientname in client_config.sections())
-
+ client_settings = Client.config_parser(client_config)
old_client_settings = {}
- clients_data = []
-
- # Get client data and settings from last running state.
+ clients_data = {}
+
+ # Get client data and settings from last running state.
if server_settings["restore"]:
try:
with open(stored_state_path, "rb") as stored_state:
- clients_data, old_client_settings = (
- pickle.load(stored_state))
+ clients_data, old_client_settings = (pickle.load
+ (stored_state))
os.remove(stored_state_path)
except IOError as e:
- logger.warning("Could not load persistant state: {0}"
+ logger.warning("Could not load persistent state: {0}"
.format(e))
if e.errno != errno.ENOENT:
raise
-
- for client in clients_data:
- client_name = client["name"]
-
- # Decide which value to use after restoring saved state.
- # We have three different values: Old config file,
- # new config file, and saved state.
- # New config value takes precedence if it differs from old
- # config value, otherwise use saved state.
- for name, value in client_settings[client_name].items():
+
+ with PGPEngine() as pgp:
+ for client_name, client in clients_data.iteritems():
+ # Decide which value to use after restoring saved state.
+ # We have three different values: Old config file,
+ # new config file, and saved state.
+ # New config value takes precedence if it differs from old
+ # config value, otherwise use saved state.
+ for name, value in client_settings[client_name].items():
+ try:
+ # For each value in new config, check if it
+ # differs from the old config value (Except for
+ # the "secret" attribute)
+ if (name != "secret" and
+ value != old_client_settings[client_name]
+ [name]):
+ client[name] = value
+ except KeyError:
+ pass
+
+ # Clients who has passed its expire date can still be
+ # enabled if its last checker was successful. Clients
+ # whose checker failed before we stored its state is
+ # assumed to have failed all checkers during downtime.
+ if client["enabled"]:
+ if datetime.datetime.utcnow() >= client["expires"]:
+ if not client["last_checked_ok"]:
+ logger.warning(
+ "disabling client {0} - Client never "
+ "performed a successfull checker"
+ .format(client["name"]))
+ client["enabled"] = False
+ elif client["last_checker_status"] != 0:
+ logger.warning(
+ "disabling client {0} - Client "
+ "last checker failed with error code {1}"
+ .format(client["name"],
+ client["last_checker_status"]))
+ client["enabled"] = False
+ else:
+ client["expires"] = (datetime.datetime
+ .utcnow()
+ + client["timeout"])
+
try:
- # For each value in new config, check if it differs
- # from the old config value (Except for the "secret"
- # attribute)
- if (name != "secret" and
- value != old_client_settings[client_name][name]):
- setattr(client, name, value)
- except KeyError:
- pass
-
- # Clients who has passed its expire date, can still be enabled
- # if its last checker was sucessful. Clients who checkers
- # failed before we stored it state is asumed to had failed
- # checker during downtime.
- if client["enabled"] and client["last_checked_ok"]:
- if ((datetime.datetime.utcnow()
- - client["last_checked_ok"]) > client["interval"]):
- if client["last_checker_status"] != 0:
- client["enabled"] = False
- else:
- client["expires"] = (datetime.datetime.utcnow()
- + client["timeout"])
-
- client["changedstate"] = (multiprocessing_manager
- .Condition(multiprocessing_manager
- .Lock()))
- if use_dbus:
- new_client = ClientDBusTransitional.__new__(
- ClientDBusTransitional)
- tcp_server.clients[client_name] = new_client
- new_client.bus = bus
- for name, value in client.iteritems():
- setattr(new_client, name, value)
- new_client._approvals_pending = 0
- new_client.add_to_dbus()
- else:
- tcp_server.clients[client_name] = Client.__new__(Client)
- for name, value in client.iteritems():
- setattr(tcp_server.clients[client_name], name, value)
-
- tcp_server.clients[client_name].decrypt_secret(
- client_settings[client_name]["secret"])
-
- # Create/remove clients based on new changes made to config
- for clientname in set(old_client_settings) - set(client_settings):
- del tcp_server.clients[clientname]
- for clientname in set(client_settings) - set(old_client_settings):
- tcp_server.clients[clientname] = client_class(name
- = clientname,
- config =
- client_settings
- [clientname])
+ client["secret"] = (
+ pgp.decrypt(client["encrypted_secret"],
+ client_settings[client_name]
+ ["secret"]))
+ except PGPError:
+ # If decryption fails, we use secret from new settings
+ logger.debug("Failed to decrypt {0} old secret"
+ .format(client_name))
+ client["secret"] = (
+ client_settings[client_name]["secret"])
+
+
+ # Add/remove clients based on new changes made to config
+ for client_name in set(old_client_settings) - set(client_settings):
+ del clients_data[client_name]
+ for client_name in set(client_settings) - set(old_client_settings):
+ clients_data[client_name] = client_settings[client_name]
+
+ # Create clients all clients
+ for client_name, client in clients_data.iteritems():
+ tcp_server.clients[client_name] = client_class(
+ name = client_name, settings = client)
if not tcp_server.clients:
logger.warning("No clients defined")
@@ -2238,7 +2300,6 @@
# "pidfile" was never created
pass
del pidfilename
-
signal.signal(signal.SIGINT, signal.SIG_IGN)
signal.signal(signal.SIGHUP, lambda signum, frame: sys.exit())
@@ -2309,43 +2370,46 @@
multiprocessing.active_children()
if not (tcp_server.clients or client_settings):
return
-
+
# Store client before exiting. Secrets are encrypted with key
# based on what config file has. If config file is
# removed/edited, old secret will thus be unrecovable.
- clients = []
- for client in tcp_server.clients.itervalues():
- client.encrypt_secret(
- client_settings[client.name]["secret"])
-
- client_dict = {}
-
- # A list of attributes that will not be stored when
- # shutting down.
- exclude = set(("bus", "changedstate", "secret"))
- for name, typ in inspect.getmembers(dbus.service.Object):
- exclude.add(name)
-
- client_dict["encrypted_secret"] = client.encrypted_secret
- for attr in client.client_structure:
- if attr not in exclude:
- client_dict[attr] = getattr(client, attr)
-
- clients.append(client_dict)
- del client_settings[client.name]["secret"]
-
+ clients = {}
+ with PGPEngine() as pgp:
+ for client in tcp_server.clients.itervalues():
+ key = client_settings[client.name]["secret"]
+ client.encrypted_secret = pgp.encrypt(client.secret,
+ key)
+ client_dict = {}
+
+ # A list of attributes that can not be pickled
+ # + secret.
+ exclude = set(("bus", "changedstate", "secret",
+ "checker"))
+ for name, typ in (inspect.getmembers
+ (dbus.service.Object)):
+ exclude.add(name)
+
+ client_dict["encrypted_secret"] = (client
+ .encrypted_secret)
+ for attr in client.client_structure:
+ if attr not in exclude:
+ client_dict[attr] = getattr(client, attr)
+
+ clients[client.name] = client_dict
+ del client_settings[client.name]["secret"]
+
try:
with os.fdopen(os.open(stored_state_path,
os.O_CREAT|os.O_WRONLY|os.O_TRUNC,
- stat.S_IRUSR | stat.S_IWUSR),
- "wb") as stored_state:
+ 0600), "wb") as stored_state:
pickle.dump((clients, client_settings), stored_state)
- except IOError as e:
- logger.warning("Could not save persistant state: {0}"
+ except (IOError, OSError) as e:
+ logger.warning("Could not save persistent state: {0}"
.format(e))
- if e.errno != errno.ENOENT:
+ if e.errno not in (errno.ENOENT, errno.EACCES):
raise
-
+
# Delete all clients, and settings from config
while tcp_server.clients:
name, client = tcp_server.clients.popitem()
@@ -2369,7 +2433,6 @@
# Need to initiate checking of clients
if client.enabled:
client.init_checker()
-
tcp_server.enable()
tcp_server.server_activate()
@@ -2415,6 +2478,5 @@
# Must run before the D-Bus bus name gets deregistered
cleanup()
-
if __name__ == '__main__':
main()
=== modified file 'mandos-clients.conf.xml'
--- mandos-clients.conf.xml 2011-10-10 20:29:58 +0000
+++ mandos-clients.conf.xml 2011-11-26 23:08:17 +0000
@@ -3,7 +3,7 @@
"http://www.oasis-open.org/docbook/xml/4.5/docbookx.dtd" [
/etc/mandos/clients.conf">
-
+
%common;
]>
@@ -344,6 +344,20 @@
+
+
+
+
+ Whether this client should be enabled by default. The
+ default is true
.
+
+
+
+
=== modified file 'mandos-options.xml'
--- mandos-options.xml 2011-11-09 17:16:03 +0000
+++ mandos-options.xml 2011-11-26 22:22:20 +0000
@@ -85,10 +85,16 @@
to the server if this option is turned off. Only
advanced users should consider changing this option.
-
+
This option controls whether the server will restore any state from
last time it ran. Default is to try restore last state.
+
+ Directory to save (and restore) state in. Default is
+ /var/lib/mandos
.
+
+
=== modified file 'mandos.conf'
--- mandos.conf 2009-02-13 05:38:21 +0000
+++ mandos.conf 2011-11-26 22:22:20 +0000
@@ -4,33 +4,27 @@
# These are the default values for the server, uncomment and change
# them if needed.
-
# If "interface" is set, the server will only listen to a specific
# network interface.
;interface =
-
# If "address" is set, the server will only listen to a specific
# address. This must currently be an IPv6 address; an IPv4 address
# can be specified using the "::FFFF:192.0.2.3" syntax. Also, if this
# is a link-local address, an interface should be set above.
;address =
-
# If "port" is set, the server to bind to that port. By default, the
# server will listen to an arbitrary port.
;port =
-
# If "debug" is true, the server will run in the foreground and print
# a lot of debugging information.
;debug = False
-
# GnuTLS priority for the TLS handshake. See gnutls_priority_init(3).
;priority = SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP
-
# Zeroconf service name. You need to change this if you for some
# reason want to run more than one server on the same *host*.
# If there are name collisions on the same *network*, the server will
@@ -42,3 +36,9 @@
# Whether to use IPv6. (Changing this is NOT recommended.)
;use_ipv6 = True
+
+# Whether to restore saved state on startup
+;restore = True
+
+# The directory where state is saved
+;statedir = /var/lib/mandos
=== modified file 'mandos.conf.xml'
--- mandos.conf.xml 2011-10-05 16:00:56 +0000
+++ mandos.conf.xml 2011-11-26 22:22:20 +0000
@@ -3,7 +3,7 @@
"http://www.oasis-open.org/docbook/xml/4.5/docbookx.dtd" [
/etc/mandos/mandos.conf">
-
+
%common;
]>
@@ -154,6 +154,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -198,6 +217,8 @@
servicename = Daena
use_dbus = False
use_ipv6 = True
+restore = True
+statedir = /var/lib/mandos
=== modified file 'mandos.xml'
--- mandos.xml 2011-11-12 18:14:55 +0000
+++ mandos.xml 2011-12-21 00:29:33 +0000
@@ -2,7 +2,7 @@
-
+
%common;
]>
@@ -96,6 +96,9 @@
+
+
&COMMANDNAME;
@@ -284,6 +287,14 @@
+
+
+
+
+
+
+
@@ -479,6 +490,20 @@
/dev/log
+
+
+ /var/lib/mandos
+
+
+ Directory where persistent state will be saved. Change
+ this with the option. See
+ also the option.
+
+
+
+
+ /dev/log
The Unix domain socket to where local syslog messages are
@@ -507,11 +532,6 @@
backtrace. This could be considered a feature.
- Currently, if a client is disabled due to having timed out, the
- server does not record this fact onto permanent storage. This
- has some security implications, see .
-
-
There is no fine-grained control over logging and debug output.
@@ -536,9 +556,9 @@
Run the server in debug mode, read configuration files from
- the ~/mandos directory, and use the
- Zeroconf service name Test
to not collide with
- any other official Mandos server on this host:
+ the ~/mandos directory,
+ and use the Zeroconf service name Test
to not
+ collide with any other official Mandos server on this host:
@@ -593,21 +613,6 @@
compromised if they are gone for too long.
- If a client is compromised, its downtime should be duly noted
- by the server which would therefore disable the client. But
- if the server was ever restarted, it would re-read its client
- list from its configuration file and again regard all clients
- therein as enabled, and hence eligible to receive their
- passwords. Therefore, be careful when restarting servers if
- it is suspected that a client has, in fact, been compromised
- by parties who may now be running a fake Mandos client with
- the keys from the non-encrypted initial RAM
- image of the client host. What should be done in that case
- (if restarting the server program really is necessary) is to
- stop the server program, edit the configuration file to omit
- any suspect clients, and restart the server program.
-
-
For more details on client-side security, see
mandos-client
8mandos.