aboutsummaryrefslogblamecommitdiff
path: root/convertsecrets
blob: b93f8c6ce76ff127b763223fafe95fc53f38ce38 (plain) (tree)





































































































































































































                                                                                                                


                                                                        















                                                                
#!/usr/bin/env nix-shell
#!nix-shell -i python3 -p "python3.withPackages(ps: [ ps.pip ps.consul ps.ldap ps.passlib ps.requests ps.six ])"

# DEPENDENCY: python-consul
import consul

# DEPENDENCY: python-ldap
import ldap

# DEPENDENCY: passlib
from passlib.hash import ldap_salted_sha1

import os
import sys
import glob
import subprocess
import getpass
import base64
from secrets import token_bytes


"""
TODO: this will be a utility to handle secrets in the Consul database
for the various components of the Deuxfleurs infrastructure

Functionnalities:
- check that secrets are correctly configured
- help user fill in secrets
- create LDAP service users and fill in corresponding secrets
- maybe one day: manage SSL certificates and keys

It uses files placed in <module_name>/secrets/* to know what secrets
it should handle. These secret files contain directives for what to do
about these secrets.

Example directives:

USER <description>
(a secret that must be filled in by the user)

USER_LONG <description>
(the same, indicates that the secret fits on several lines)

CMD <command>
(a secret that is generated by running this command)

CMD_ONCE <command>
(same, but value is not changed when doing a regen)

CONST <constant value>
(the secret has a constant value set here)

CONST_LONG
<constant value, several lines>
(same)

SERVICE_DN <service name> <service description>
(the LDAP DN of a service user)

SERVICE_PASSWORD <service name>
(the LDAP password for the corresponding service user)

SSL_CERT <cert name> <list of domains>
(a SSL domain for the given domains)

SSL_KEY <cert name>
(the SSL key going with corresponding certificate)

RSA_PUBLIC_KEY <key name> <key description>
(a public RSA key)

RSA_PRIVATE_KEY <key name>
(the corresponding private RSA key)
"""


# Parameters
LDAP_URL = "ldap://localhost:1389"
SERVICE_DN_SUFFIX = "ou=services,ou=users,dc=deuxfleurs,dc=fr"
consul_server = consul.Consul()


# ----

USER             = "USER"
USER_LONG        = "USER_LONG"
CMD              = "CMD"
CMD_ONCE         = "CMD_ONCE"
CONST            = "CONST"
CONST_LONG       = "CONST_LONG"
SERVICE_DN       = "SERVICE_DN"
SERVICE_PASSWORD = "SERVICE_PASSWORD"
SSL_CERT         = "SSL_CERT"
SSL_KEY          = "SSL_KEY"
RSA_PUBLIC_KEY   = "RSA_PUBLIC_KEY"
RSA_PRIVATE_KEY  = "RSA_PRIVATE_KEY"

class bcolors:
    HEADER = '\033[95m'
    OKBLUE = '\033[94m'
    OKCYAN = '\033[96m'
    OKGREEN = '\033[92m'
    WARNING = '\033[93m'
    FAIL = '\033[91m'
    ENDC = '\033[0m'
    BOLD = '\033[1m'
    UNDERLINE = '\033[4m'

def read_secret(key, file_path):
    lines = [l.strip() for l in open(file_path, "r")]
    if len(lines) == 0:
        print(bcolors.FAIL, "ERROR:", bcolors.ENDC, "Empty file in", file_path)
        sys.exit(-1)
    l0 = lines[0].split(" ")
    stype = l0[0]
    secret = {"type": stype, "key": key}
    if stype in [USER, USER_LONG]:
        secret["desc"] = " ".join(l0[1:])
    elif stype in [CMD, CMD_ONCE]:
        secret["cmd"] = " ".join(l0[1:])
    elif stype == CONST:
        secret["value"] = " ".join(l0[1:])
    elif stype == CONST_LONG:
        secret["value"] = "\n".join(lines[1:])
    elif stype in [SERVICE_DN, SERVICE_PASSWORD]:
        secret["service"] = l0[1]
        if stype == SERVICE_DN:
            secret["service_desc"] = " ".join(l0[2:])
    elif stype in [SSL_CERT, SSL_KEY]:
        secret["cert_name"] = l0[1]
        if stype == SSL_CERT:
            secret["cert_domains"] = l0[2:]
    elif stype in [RSA_PUBLIC_KEY, RSA_PRIVATE_KEY]:
        secret["key_name"] = l0[1]
        if stype == RSA_PUBLIC_KEY:
            secret["key_desc"] = " ".join(l0[2:])
    else:
        print(bcolors.FAIL, "ERROR:", bcolors.ENDC, "Invalid secret type", stype, "in", file_path)
        sys.exit(-1)

    return secret

def read_secrets(module_list):
    secrets = {}
    for mod in module_list:
        for file_path in glob.glob(mod.strip('/') + "/secrets/**", recursive=True):
            if os.path.isfile(file_path):
                key = '/'.join(file_path.split("/")[1:])
                secrets[key] = read_secret(key, file_path)
    return secrets

def convert_secrets(module_list):
    for mod in module_list:
        print("converting module: ", mod)
        secrets = read_secrets([mod])
        with open(os.path.join(mod.strip('/'), "secrets.toml"), "w") as file:
            for (secret_key, secret) in secrets.items():
                file.write("[secrets.\"{}\"]\n".format("/".join(secret_key.split("/")[1:])))
                ty = secret["type"]
                if ty in [CMD, CMD_ONCE]:
                    newsecret = {
                            "type": "command",
                            "rotate": ty != "CMD_ONCE",
                            "command": secret["cmd"],
                        }
                elif ty in [USER, USER_LONG]:
                    newsecret = {
                            "type": "user",
                            "multiline": ty == "USER_LONG",
                            "description": secret["desc"],
                        }
                elif ty in [CONST, CONST_LONG]:
                    newsecret = {
                            "type": "constant",
                            "value": secret["value"],
                        }
                else:
                    newsecret = {
                            "type": secret["type"],
                    }
                    if "service_desc" in secret:
                        newsecret["description"] = secret["service_desc"]
                    if "key_desc" in secret:
                        newsecret["description"] = secret["key_desc"]
                    if "cert_name" in secret:
                        newsecret["name"] = secret["cert_name"]
                    if "key_name" in secret:
                        newsecret["name"] = secret["key_name"]
                    for k in ["service", "cert_domains"]:
                        if k in secret:
                            newsecret[k] = secret[k]
                for (k, v) in newsecret.items():
                    if type(v) == bool:
                        if v:
                            file.write("{} = true\n".format(k))
                    elif type(v) == str:
                        file.write("{} = {}\n".format(k, repr(v)))
                    else:
                        print("warning: invalid value: ", v)
                        print("secret to fix: ", k)
                        file.write("{} = {}\n".format(k, repr(repr(v))))
                file.write("\n")


# ---- MAIN ----

if __name__ == "__main__":
    for i, val in enumerate(sys.argv):
        if val == "convert":
            convert_secrets(sys.argv[i+1:])
            break
        else:
            print("Usage:")
            print("    convertsecrets convert <module name>...")


#  vim: set sts=4 ts=4 sw=4 tw=0 ft=python et :