From cc5ef01e7fd5fa23e957e59d8549d734eb087174 Mon Sep 17 00:00:00 2001
From: ale <ale@incal.net>
Date: Sat, 1 Sep 2018 08:21:54 +0100
Subject: [PATCH] Support includes in passwords.yml

Distribute a default passwords.yml file for users to include. Mirrors
the setup used for services.yml.
---
 passwords.yml.default          | 43 ++++++++++++++++
 playbooks/init-credentials.yml |  4 ++
 scripts/pwgen.py               | 89 +++++++++++++++++++++++++---------
 3 files changed, 112 insertions(+), 24 deletions(-)
 create mode 100644 passwords.yml.default

diff --git a/passwords.yml.default b/passwords.yml.default
new file mode 100644
index 00000000..01645e0a
--- /dev/null
+++ b/passwords.yml.default
@@ -0,0 +1,43 @@
+- name: sso_session_auth_secret
+  description: sso-server cookie auth key
+  type: binary
+  length: 64
+- name: sso_session_enc_secret
+  description: sso-server cookie encryption key
+  type: binary
+  length: 16
+- name: sso_csrf_secret
+  description: sso-server cookie-based CSRF secret
+  type: binary
+  length: 64
+- name: sso_device_manager_auth_secret
+  description: sso-server cookie-based device manager secret
+  type: binary
+  length: 64
+
+- name: ssoproxy_session_auth_key
+  description: sso-proxy cookie authentication key
+  type: binary
+  length: 64
+- name: ssoproxy_session_enc_key
+  description: sso-proxy cookie encryption key
+  type: binary
+  length: 32
+
+- name: ldap_root_password
+  description: LDAP cn=manager password
+- name: ldap_replica_password
+  description: LDAP cn=replica password
+- name: ldap_authserver_password
+  description: LDAP cn=authserver password
+- name: ldap_keystore_password
+  description: LDAP cn=keystore password
+- name: ldap_accountserver_password
+  description: LDAP cn=accountserver password
+
+- name: grafana_session_secret
+  description: session secret for Grafana
+  length: 32
+
+- name: acme_tsig_key
+  type: tsig
diff --git a/playbooks/init-credentials.yml b/playbooks/init-credentials.yml
index f4d5ab01..a7b005d6 100644
--- a/playbooks/init-credentials.yml
+++ b/playbooks/init-credentials.yml
@@ -27,6 +27,10 @@
     # First of all, generate secrets from the passwords.yml file.
     - name: Initialize secrets
       local_action: command ../scripts/pwgen.py --vars "{{ credentials_dir }}/secrets.yml" "{{ passwords_file }}"
+      register: pwgen_result
+      changed_when: "pwgen_result.rc == 1"
+      failed_when: "pwgen_result.rc > 1"
+
     - name: Link secrets.yml from the vars directory
       file:
         src: "{{ credentials_dir }}/secrets.yml"
diff --git a/scripts/pwgen.py b/scripts/pwgen.py
index d7052e5c..95c71e5b 100755
--- a/scripts/pwgen.py
+++ b/scripts/pwgen.py
@@ -6,18 +6,49 @@
 
 from __future__ import print_function
 
+import argparse
 import base64
-import sys
-import optparse
 import os
 import random
 import shutil
 import subprocess
 import string
+import sys
 import tempfile
 import yaml
 
 
+# Possible exit codes for this program.
+EXIT_NOTHING_TO_DO = 0
+EXIT_CHANGED = 1
+EXIT_ERROR = 2
+
+
+# Returns the absolute path to a file. If the given path is relative,
+# it will be evaluated based on the given path_reference.
+def _abspath(path, relative_to='/'):
+    if path.startswith('/'):
+        return path
+    return os.path.abspath(os.path.join(os.path.dirname(relative_to), path))
+
+
+# A version of yaml.safe_load that supports the special 'include'
+# top-level attribute and recursively merges included files.
+def _read_yaml(path):
+    with open(path) as fd:
+        data = yaml.safe_load(fd)
+    if not isinstance(data, list):
+        raise Exception('data in %s is not a list' % (path,))
+    # Find elements that include other files.
+    out = []
+    for entry in data:
+        if 'include' in entry:
+            out.extend(_read_yaml(_abspath(entry['include'], path)))
+        else:
+            out.append(entry)
+    return out
+
+
 def decrypt(src):
     return subprocess.check_output(
         ['ansible-vault', 'decrypt', '--output=-', src])
@@ -99,38 +130,48 @@ def generate_password(entry):
 
 
 def main():
-    parser = optparse.OptionParser(usage='%prog <password file>')
-    parser.add_option('--vars', metavar='FILE', dest='vars',
-                      default='vars/passwords',
-                      help='Output vars file')
-    opts, args = parser.parse_args()
-    if len(args) != 1:
-        parser.error('Not enough arguments')
+    parser = argparse.ArgumentParser(description='''
+Autogenerate secrets for use with Ansible.
+
+Secrets are encrypted with Ansible Vault, so the
+ANSIBLE_VAULT_PASSWORD_FILE environment variable must be defined.
+''')
+    parser.add_argument(
+        '--vars', metavar='FILE', dest='vars_file',
+        help='Output vars file')
+    parser.add_argument(
+        'password_file',
+        help='Secrets metadata')
+    args = parser.parse_args()
 
     if not os.getenv('ANSIBLE_VAULT_PASSWORD_FILE'):
-        print("You need to set ANSIBLE_VAULT_PASSWORD_FILE", file=sys.stderr)
-        sys.exit(2)
+        raise Exception("You need to set ANSIBLE_VAULT_PASSWORD_FILE")
 
-    password_file = args[0]
-    vars_file = opts.vars
     passwords = {}
 
-    if os.path.exists(vars_file):
-        passwords.update(yaml.load(decrypt(vars_file)))
+    if os.path.exists(args.vars_file):
+        passwords.update(yaml.safe_load(decrypt(args.vars_file)))
 
     changed = False
-    with open(password_file) as fd:
-        for entry in yaml.load(fd):
-            name = entry['name']
 
-            if name not in passwords:
-                print("Generating password for '%s'" % name, file=sys.stderr)
-                passwords[name] = generate_password(entry)
-                changed = True
+    for entry in _read_yaml(args.password_file):
+        name = entry['name']
+        if name not in passwords:
+            print("Generating password for '%s'" % name, file=sys.stderr)
+            passwords[name] = generate_password(entry)
+            changed = True
 
     if changed:
-        encrypt(yaml.dump(passwords), vars_file)
+        encrypt(yaml.dump(passwords), args.vars_file)
+        return EXIT_CHANGED
+
+    return EXIT_NOTHING_TO_DO
 
 
 if __name__ == '__main__':
-    main()
+    try:
+        sys.exit(main())
+    except Exception as e:
+        print("Error: %s" % str(e), file=sys.stderr)
+        sys.exit(EXIT_ERROR)
+
-- 
GitLab