Repository: ansible/ansible

Issue:
Ansible generated passwords would benefit from additional entropy
The passwords generated by Ansible use random strings from Python's random module (in `lookup_plugins/password.py` and in `library/cloud/linode`.  However, that's not a cryptographically secure source of random data.

Frank Sievertsen showed how an attacker who knows some amount of output from the Python random module can reconstruct the internal state of the generator, and then predict all past and future random outputs from it.  https://spideroak.com/blog/20121205114003-exploit-information-leaks-in-random-numbers-from-python-ruby-and-php

This probably only matters in rather contrived situations, such as when many passwords are generated in a single ansible run and an attacker knows many of those passwords.  They could then use that information to calculate other passwords. 

The fix is trivial; pull request incoming.



Discussion:

Resolving patch:
diff --git a/lib/ansible/constants.py b/lib/ansible/constants.py
--- a/lib/ansible/constants.py
+++ b/lib/ansible/constants.py
@@ -19,6 +19,7 @@
 import pwd
 import sys
 import ConfigParser
+from string import ascii_letters, digits
 
 # copied from utils, avoid circular reference fun :)
 def mk_boolean(value):
@@ -148,6 +149,8 @@ def shell_expand_path(path):
 ACCELERATE_CONNECT_TIMEOUT     = get_config(p, 'accelerate', 'accelerate_connect_timeout', 'ACCELERATE_CONNECT_TIMEOUT', 1.0, floating=True)
 PARAMIKO_PTY                   = get_config(p, 'paramiko_connection', 'pty', 'ANSIBLE_PARAMIKO_PTY', True, boolean=True)
 
+# characters included in auto-generated passwords
+DEFAULT_PASSWORD_CHARS = ascii_letters + digits + ".,:-_"
 
 # non-configurable things
 DEFAULT_SUDO_PASS         = None
diff --git a/lib/ansible/runner/lookup_plugins/password.py b/lib/ansible/runner/lookup_plugins/password.py
--- a/lib/ansible/runner/lookup_plugins/password.py
+++ b/lib/ansible/runner/lookup_plugins/password.py
@@ -20,7 +20,6 @@
 from ansible import utils, errors
 import os
 import errno
-import random
 from string import ascii_letters, digits
 
 
@@ -33,10 +32,7 @@ def __init__(self, length=None, encrypt=None, basedir=None, **kwargs):
 
     def random_salt(self):
         salt_chars = ascii_letters + digits + './'
-        salt = []
-        for _ in range(8):
-            salt.append(random.choice(salt_chars))
-        return ''.join(salt)
+        return utils.random_password(length=8, chars=salt_chars)
 
     def run(self, terms, inject=None, **kwargs):
 
@@ -76,7 +72,7 @@ def run(self, terms, inject=None, **kwargs):
                 if not os.path.isdir(pathdir):
                     os.makedirs(pathdir)
                 chars = ascii_letters + digits + ".,:-_"
-                password = ''.join(random.choice(chars) for _ in range(length))
+                password = utils.random_password(length)
                 if encrypt is not None:
                     salt = self.random_salt()
                     content = '%s salt=%s' % (password, salt)
diff --git a/lib/ansible/utils/__init__.py b/lib/ansible/utils/__init__.py
--- a/lib/ansible/utils/__init__.py
+++ b/lib/ansible/utils/__init__.py
@@ -1006,4 +1006,13 @@ def combine_vars(a, b):
     else:
         return dict(a.items() + b.items())
 
+def random_password(length=20, chars=C.DEFAULT_PASSWORD_CHARS):
+    '''Return a random password string of length containing only chars.'''
 
+    password = []
+    while len(password) < length:
+        new_char = os.urandom(1)
+        if new_char in chars:
+            password.append(new_char)
+
+    return ''.join(password)


Test patch: