feat: add inner password storage and optimize flask command about inner cmdb (#248)

Co-authored-by: fxiang21 <fxiang21@126.com>
This commit is contained in:
pycook 2023-10-30 16:48:53 +08:00 committed by GitHub
parent c9f0de9838
commit 5b314aa907
5 changed files with 124 additions and 93 deletions

View File

@ -318,6 +318,17 @@ def cmdb_index_table_upgrade():
db.session.commit() db.session.commit()
def valid_address(address):
if not address.startswith(("http://127.0.0.1", "https://127.0.0.1")):
response = {
"message": "Address should start with http://127.0.0.1 or https://127.0.0.1",
"status": "failed"
}
KeyManage.print_response(response)
return False
return True
@click.command() @click.command()
@click.option( @click.option(
'-a', '-a',
@ -329,15 +340,26 @@ def cmdb_inner_secrets_init(address):
""" """
init inner secrets for password feature init inner secrets for password feature
""" """
KeyManage(backend=InnerKVManger).init() res, ok = KeyManage(backend=InnerKVManger).init()
if not ok:
if res.get("status") == "failed":
KeyManage.print_response(res)
return
if address and address.startswith("http") and current_app.config.get("INNER_TRIGGER_TOKEN", "") != "": token = res.get("details", {}).get("root_token", "")
if valid_address(address):
token = current_app.config.get("INNER_TRIGGER_TOKEN", "") if not token else token
if not token:
token = click.prompt(f'Enter root token', hide_input=True, confirmation_prompt=False)
assert token is not None
resp = requests.post("{}/api/v0.1/secrets/auto_seal".format(address.strip("/")), resp = requests.post("{}/api/v0.1/secrets/auto_seal".format(address.strip("/")),
headers={"Inner-Token": current_app.config.get("INNER_TRIGGER_TOKEN", "")}) headers={"Inner-Token": token})
if resp.status_code == 200: if resp.status_code == 200:
KeyManage.print_response(resp.json()) KeyManage.print_response(resp.json())
else: else:
KeyManage.print_response({"message": resp.text, "status": "failed"}) KeyManage.print_response({"message": resp.text or resp.status_code, "status": "failed"})
else:
KeyManage.print_response(res)
@click.command() @click.command()
@ -352,18 +374,19 @@ def cmdb_inner_secrets_unseal(address):
""" """
unseal the secrets feature unseal the secrets feature
""" """
address = "{}/api/v0.1/secrets/unseal".format(address.strip("/")) if not valid_address(address):
if not address.startswith("http"):
KeyManage.print_response({"message": "invalid address, should start with http", "status": "failed"})
return return
address = "{}/api/v0.1/secrets/unseal".format(address.strip("/"))
for i in range(global_key_threshold): for i in range(global_key_threshold):
token = click.prompt(f'Enter unseal token {i + 1}', hide_input=True, confirmation_prompt=False) token = click.prompt(f'Enter unseal token {i + 1}', hide_input=True, confirmation_prompt=False)
assert token is not None assert token is not None
resp = requests.post(address, headers={"Unseal-Token": token}) resp = requests.post(address, headers={"Unseal-Token": token})
if resp.status_code == 200: if resp.status_code == 200:
KeyManage.print_response(resp.json()) KeyManage.print_response(resp.json())
if resp.json().get("status") in ["success", "skip"]:
return
else: else:
KeyManage.print_response({"message": resp.text, "status": "failed"}) KeyManage.print_response({"message": resp.status_code, "status": "failed"})
return return
@ -388,7 +411,8 @@ def cmdb_inner_secrets_seal(address, token):
""" """
assert address is not None assert address is not None
assert token is not None assert token is not None
if address.startswith("http"): if not valid_address(address):
return
address = "{}/api/v0.1/secrets/seal".format(address.strip("/")) address = "{}/api/v0.1/secrets/seal".format(address.strip("/"))
resp = requests.post(address, headers={ resp = requests.post(address, headers={
"Inner-Token": token, "Inner-Token": token,
@ -396,7 +420,7 @@ def cmdb_inner_secrets_seal(address, token):
if resp.status_code == 200: if resp.status_code == 200:
KeyManage.print_response(resp.json()) KeyManage.print_response(resp.json())
else: else:
KeyManage.print_response({"message": resp.text, "status": "failed"}) KeyManage.print_response({"message": resp.status_code, "status": "failed"})
@click.command() @click.command()

View File

@ -9,12 +9,10 @@ from flask_login import LoginManager
from flask_migrate import Migrate from flask_migrate import Migrate
from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy
from api.lib.secrets.inner import KeyManage
from api.lib.utils import ESHandler from api.lib.utils import ESHandler
from api.lib.utils import RedisHandler from api.lib.utils import RedisHandler
from api.lib.secrets.inner import KeyManage
bcrypt = Bcrypt() bcrypt = Bcrypt()
login_manager = LoginManager() login_manager = LoginManager()
db = SQLAlchemy(session_options={"autoflush": False}) db = SQLAlchemy(session_options={"autoflush": False})

View File

@ -1,8 +1,4 @@
import os
import secrets
import sys
from base64 import b64decode, b64encode from base64 import b64decode, b64encode
from colorama import Back from colorama import Back
from colorama import Fore from colorama import Fore
from colorama import init as colorama_init from colorama import init as colorama_init
@ -17,6 +13,9 @@ from cryptography.hazmat.primitives.ciphers import modes
from cryptography.hazmat.primitives.ciphers.aead import AESGCM from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from flask import current_app from flask import current_app
import os
import secrets
import sys
global_iv_length = 16 global_iv_length = 16
global_key_shares = 5 # Number of generated key shares global_key_shares = 5 # Number of generated key shares
@ -26,6 +25,7 @@ backend_root_key_name = "root_key"
backend_encrypt_key_name = "encrypt_key" backend_encrypt_key_name = "encrypt_key"
backend_root_key_salt_name = "root_key_salt" backend_root_key_salt_name = "root_key_salt"
backend_encrypt_key_salt_name = "encrypt_key_salt" backend_encrypt_key_salt_name = "encrypt_key_salt"
backend_seal_key = "seal_status"
success = "success" success = "success"
seal_status = True seal_status = True
@ -51,6 +51,9 @@ class Backend:
def add(self, key, value): def add(self, key, value):
return self.backend.add(key, value) return self.backend.add(key, value)
def update(self, key, value):
return self.backend.update(key, value)
class KeyManage: class KeyManage:
@ -61,11 +64,11 @@ class KeyManage:
self.backend = Backend(backend) self.backend = Backend(backend)
def init_app(self, app, backend=None): def init_app(self, app, backend=None):
if sys.argv[0].endswith("gunicorn") or sys.argv[1] == "run":
self.trigger = app.config.get("INNER_TRIGGER_TOKEN") self.trigger = app.config.get("INNER_TRIGGER_TOKEN")
if not self.trigger: if not self.trigger:
return return
self.backend = backend self.backend = backend
resp = self.auto_unseal() resp = self.auto_unseal()
self.print_response(resp) self.print_response(resp)
@ -118,23 +121,23 @@ class KeyManage:
return new_shares return new_shares
def auth_root_secret(self, root_key): def is_valid_root_key(self, root_key):
root_key_hash, ok = self.hash_root_key(root_key) root_key_hash, ok = self.hash_root_key(root_key)
if not ok: if not ok:
return { return root_key_hash, ok
"message": root_key_hash,
"status": "failed"
}
backend_root_key_hash = self.backend.get(backend_root_key_name) backend_root_key_hash = self.backend.get(backend_root_key_name)
if not backend_root_key_hash: if not backend_root_key_hash:
return { return "should init firstly", False
"message": "should init firstly",
"status": "failed"
}
elif backend_root_key_hash != root_key_hash: elif backend_root_key_hash != root_key_hash:
return "invalid root key", False
else:
return "", True
def auth_root_secret(self, root_key):
msg, ok = self.is_valid_root_key(root_key)
if not ok:
return { return {
"message": "invalid root key", "message": msg,
"status": "failed" "status": "failed"
} }
@ -146,11 +149,14 @@ class KeyManage:
} }
secrets_encrypt_key, ok = InnerCrypt.aes_decrypt(string_to_bytes(root_key), encrypt_key_aes) secrets_encrypt_key, ok = InnerCrypt.aes_decrypt(string_to_bytes(root_key), encrypt_key_aes)
if ok:
msg, ok = self.backend.update(backend_seal_key, "open")
if ok: if ok:
current_app.config["secrets_encrypt_key"] = secrets_encrypt_key current_app.config["secrets_encrypt_key"] = secrets_encrypt_key
current_app.config["secrets_root_key"] = root_key current_app.config["secrets_root_key"] = root_key
current_app.config["secrets_shares"] = [] current_app.config["secrets_shares"] = []
return {"message": success, "status": success} return {"message": success, "status": success}
return {"message": msg, "status": "failed"}
else: else:
return { return {
"message": secrets_encrypt_key, "message": secrets_encrypt_key,
@ -204,34 +210,36 @@ class KeyManage:
""" """
root_key = self.backend.get(backend_root_key_name) root_key = self.backend.get(backend_root_key_name)
if root_key: if root_key:
return {"message": "already init, skip"}, False return {"message": "already init, skip", "status": "skip"}, False
else: else:
root_key, shares, status = self.generate_unseal_keys() root_key, shares, status = self.generate_unseal_keys()
if not status: if not status:
return {"message": root_key}, False return {"message": root_key, "status": "failed"}, False
# hash root key and store in backend # hash root key and store in backend
root_key_hash, ok = self.hash_root_key(root_key) root_key_hash, ok = self.hash_root_key(root_key)
if not ok: if not ok:
return {"message": root_key_hash}, False return {"message": root_key_hash, "status": "failed"}, False
msg, ok = self.backend.add(backend_root_key_name, root_key_hash) msg, ok = self.backend.add(backend_root_key_name, root_key_hash)
if not ok: if not ok:
return {"message": msg}, False return {"message": msg, "status": "failed"}, False
# generate encrypt key from root_key and store in backend # generate encrypt key from root_key and store in backend
encrypt_key, ok = self.generate_encrypt_key(root_key) encrypt_key, ok = self.generate_encrypt_key(root_key)
if not ok: if not ok:
return {"message": encrypt_key} return {"message": encrypt_key, "status": "failed"}
encrypt_key_aes, status = InnerCrypt.aes_encrypt(root_key, encrypt_key) encrypt_key_aes, status = InnerCrypt.aes_encrypt(root_key, encrypt_key)
if not status: if not status:
return {"message": encrypt_key_aes} return {"message": encrypt_key_aes, "status": "failed"}
msg, ok = self.backend.add(backend_encrypt_key_name, encrypt_key_aes) msg, ok = self.backend.add(backend_encrypt_key_name, encrypt_key_aes)
if not ok: if not ok:
return {"message": msg}, False return {"message": msg, "status": "failed"}, False
msg, ok = self.backend.add(backend_seal_key, "open")
if not ok:
return {"message": msg, "status": "failed"}, False
current_app.config["secrets_root_key"] = root_key current_app.config["secrets_root_key"] = root_key
current_app.config["secrets_encrypt_key"] = encrypt_key current_app.config["secrets_encrypt_key"] = encrypt_key
self.print_token(shares, root_token=root_key) self.print_token(shares, root_token=root_key)
@ -275,28 +283,21 @@ class KeyManage:
def seal(self, root_key): def seal(self, root_key):
root_key = root_key.encode() root_key = root_key.encode()
root_key_hash, ok = self.hash_root_key(root_key) msg, ok = self.is_valid_root_key(root_key)
if not ok: if not ok:
return { return {
"message": root_key_hash, "message": msg,
"status": "failed"
}
backend_root_key_hash = self.backend.get(backend_root_key_name)
if not backend_root_key_hash:
return {
"message": "not init, seal skip",
"status": "skip"
}
elif root_key_hash != backend_root_key_hash:
return {
"message": "invalid root key",
"status": "failed" "status": "failed"
} }
else: else:
msg, ok = self.backend.update(backend_seal_key, "block")
if not ok:
return {
"message": msg,
"status": "failed",
}
current_app.config["secrets_root_key"] = '' current_app.config["secrets_root_key"] = ''
current_app.config["secrets_encrypt_key"] = '' current_app.config["secrets_encrypt_key"] = ''
return { return {
"message": success, "message": success,
"status": success "status": success
@ -308,11 +309,11 @@ class KeyManage:
:return: :return:
""" """
secrets_root_key = current_app.config.get("secrets_root_key") secrets_root_key = current_app.config.get("secrets_root_key")
root_key = self.backend.get(backend_root_key_name) msg, ok = self.is_valid_root_key(secrets_root_key)
if root_key == "" or root_key != secrets_root_key: if not ok:
return "invalid root key", True return {"message": msg, "status": "failed"}
status = self.backend.get(backend_seal_key)
return "", False return status == "block"
@classmethod @classmethod
def print_token(cls, shares, root_token): def print_token(cls, shares, root_token):
@ -330,7 +331,7 @@ class KeyManage:
for i, v in enumerate(shares): for i, v in enumerate(shares):
print( print(
"unseal token " + str(i + 1) + ": " + Fore.RED + Back.CYAN + v.decode("utf-8") + Style.RESET_ALL) "unseal token " + str(i + 1) + ": " + Fore.RED + Back.BLACK + v.decode("utf-8") + Style.RESET_ALL)
print() print()
print(Fore.GREEN + "root token: " + root_token.decode("utf-8") + Style.RESET_ALL) print(Fore.GREEN + "root token: " + root_token.decode("utf-8") + Style.RESET_ALL)
@ -339,14 +340,12 @@ class KeyManage:
def print_response(cls, data): def print_response(cls, data):
status = data.get("status", "") status = data.get("status", "")
message = data.get("message", "") message = data.get("message", "")
if status == "skip": status_colors = {
print(Style.BRIGHT, message) "skip": Style.BRIGHT,
elif status == "failed": "failed": Fore.RED,
print(Fore.RED, message) "waiting": Fore.YELLOW,
elif status == "waiting": }
print(Fore.YELLOW, message) print(status_colors.get(status, Fore.GREEN), message, Style.RESET_ALL)
else:
print(Fore.GREEN, message)
class InnerCrypt: class InnerCrypt:

View File

@ -11,7 +11,6 @@ class InnerKVManger(object):
res = InnerKV.create(**data) res = InnerKV.create(**data)
if res.key == key: if res.key == key:
return "success", True return "success", True
return "add failed", False return "add failed", False
@classmethod @classmethod
@ -21,3 +20,14 @@ class InnerKVManger(object):
return None return None
return res.value return res.value
@classmethod
def update(cls, key, value):
res = InnerKV.get_by(first=True, to_dict=False, **{"key": key})
if not res:
return None
res.value = value
t = res.update()
if t.key == key:
return "success", True
return "update failed", True

View File

@ -1,38 +1,38 @@
from api.lib.perm.auth import auth_abandoned
from api.resource import APIView from api.resource import APIView
from api.lib.secrets.inner import KeyManage from api.lib.secrets.inner import KeyManage
from api.lib.secrets.secrets import InnerKVManger from api.lib.secrets.secrets import InnerKVManger
from flask import request, abort from flask import current_app
from flask import request
class InnerSecretUnSealView(APIView): class InnerSecretUnSealView(APIView):
url_prefix = "/secrets/unseal" url_prefix = "/secrets/unseal"
@auth_abandoned
def post(self): def post(self):
unseal_key = request.headers.get("Unseal-Token") unseal_key = request.headers.get("Unseal-Token")
res = KeyManage(backend=InnerKVManger()).unseal(unseal_key) res = KeyManage(backend=InnerKVManger()).unseal(unseal_key)
# if res.get("status") == "failed":
# return abort(400, res.get("message"))
return self.jsonify(**res) return self.jsonify(**res)
class InnerSecretSealView(APIView): class InnerSecretSealView(APIView):
url_prefix = "/secrets/seal" url_prefix = "/secrets/seal"
@auth_abandoned
def post(self): def post(self):
unseal_key = request.headers.get("Inner-Token") unseal_key = request.headers.get("Inner-Token")
res = KeyManage(backend=InnerKVManger()).seal(unseal_key) res = KeyManage(backend=InnerKVManger()).seal(unseal_key)
# if res.get("status") == "failed":
# return abort(400, res.get("message"))
return self.jsonify(**res) return self.jsonify(**res)
class InnerSecretAutoSealView(APIView): class InnerSecretAutoSealView(APIView):
url_prefix = "/secrets/auto_seal" url_prefix = "/secrets/auto_seal"
@auth_abandoned
def post(self): def post(self):
unseal_key = request.headers.get("Inner-Token") root_key = request.headers.get("Inner-Token")
res = KeyManage(backend=InnerKVManger()).seal(unseal_key) res = KeyManage(trigger=root_key,
# if res.get("status") == "failed": backend=InnerKVManger()).auto_unseal()
# return abort(400, res.get("message"))
return self.jsonify(**res) return self.jsonify(**res)