feat: add secrets feature

This commit is contained in:
fxiang21 2023-10-28 13:31:58 +08:00
parent bfb1cb14b3
commit 7bc62ba66b
7 changed files with 123 additions and 67 deletions

View File

@ -21,6 +21,7 @@ from api.extensions import (bcrypt, cache, celery, cors, db, es, login_manager,
from api.extensions import inner_secrets from api.extensions import inner_secrets
from api.flask_cas import CAS from api.flask_cas import CAS
from api.models.acl import User from api.models.acl import User
from api.lib.secrets.secrets import InnerKVManger
HERE = os.path.abspath(os.path.dirname(__file__)) HERE = os.path.abspath(os.path.dirname(__file__))
PROJECT_ROOT = os.path.join(HERE, os.pardir) PROJECT_ROOT = os.path.join(HERE, os.pardir)
@ -126,7 +127,7 @@ def register_extensions(app):
app.config.update(app.config.get("CELERY")) app.config.update(app.config.get("CELERY"))
celery.conf.update(app.config) celery.conf.update(app.config)
inner_secrets.init_app(app) inner_secrets.init_app(app, InnerKVManger())
def register_blueprints(app): def register_blueprints(app):

View File

@ -7,6 +7,7 @@ import json
import time import time
import click import click
import requests
from flask import current_app from flask import current_app
from flask.cli import with_appcontext from flask.cli import with_appcontext
from flask_login import login_user from flask_login import login_user
@ -318,28 +319,61 @@ def cmdb_index_table_upgrade():
@click.command() @click.command()
@click.option(
'-a',
'--address',
help='inner cmdb api, http://127.0.0.1:8000',
)
@with_appcontext @with_appcontext
def cmdb_inner_secrets_init(): def cmdb_inner_secrets_init(address):
""" """
init inner secrets for password feature init inner secrets for password feature
""" """
KeyManage(backend=InnerKVManger).init() KeyManage(backend=InnerKVManger).init()
if address and address.startswith("http") and current_app.config.get("INNER_TRIGGER_TOKEN", "") != "":
resp = requests.post("{}/api/v0.1/secrets/auto_seal".format(address.strip("/")),
headers={"Inner-Token": current_app.config.get("INNER_TRIGGER_TOKEN", "")})
if resp.status_code == 200:
KeyManage.print_response(resp.json())
else:
KeyManage.print_response({"message": resp.text, "status": "failed"})
@click.command() @click.command()
@click.option(
'-a',
'--address',
help='inner cmdb api, http://127.0.0.1:8000',
required=True,
)
@with_appcontext @with_appcontext
def cmdb_inner_secrets_unseal(): 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 address.startswith("http"):
KeyManage.print_response({"message": "invalid address, should start with http", "status": "failed"})
return
for i in range(global_key_threshold): for i in range(global_key_threshold):
token = click.prompt(f'Enter 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
res = KeyManage(backend=InnerKVManger).unseal(token) resp = requests.post(address, headers={"Unseal-Token": token})
KeyManage.print_response(res) if resp.status_code == 200:
KeyManage.print_response(resp.json())
else:
KeyManage.print_response({"message": resp.text, "status": "failed"})
return
@click.command() @click.command()
@click.option(
'-a',
'--address',
help='inner cmdb api, http://127.0.0.1:8000',
required=True,
)
@click.option( @click.option(
'-k', '-k',
'--token', '--token',
@ -348,23 +382,21 @@ def cmdb_inner_secrets_unseal():
hide_input=True, hide_input=True,
) )
@with_appcontext @with_appcontext
def cmdb_inner_secrets_seal(token): def cmdb_inner_secrets_seal(address, token):
""" """
seal the secrets feature seal the secrets feature
""" """
assert address is not None
assert token is not None assert token is not None
res = KeyManage(backend=InnerKVManger()).seal(token) if address.startswith("http"):
KeyManage.print_response(res) address = "{}/api/v0.1/secrets/seal".format(address.strip("/"))
resp = requests.post(address, headers={
"Inner-Token": token,
@click.command() })
@with_appcontext if resp.status_code == 200:
def cmdb_inner_secrets_auto_seal(): KeyManage.print_response(resp.json())
""" else:
auto seal the secrets feature KeyManage.print_response({"message": resp.text, "status": "failed"})
"""
res = KeyManage(current_app.config.get("INNER_TRIGGER_TOKEN"), backend=InnerKVManger()).auto_unseal()
KeyManage.print_response(res)
@click.command() @click.command()

View File

@ -14,6 +14,7 @@ from api.lib.utils import RedisHandler
from api.lib.secrets.inner import KeyManage 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

@ -60,8 +60,13 @@ class KeyManage:
if backend: if backend:
self.backend = Backend(backend) self.backend = Backend(backend)
def init_app(self, app): def init_app(self, app, backend=None):
self.auto_unseal() self.trigger = app.config.get("INNER_TRIGGER_TOKEN")
if not self.trigger:
return
self.backend = backend
# resp = self.auto_unseal()
# self.print_response(resp)
def hash_root_key(self, value): def hash_root_key(self, value):
algorithm = hashes.SHA256() algorithm = hashes.SHA256()
@ -133,14 +138,16 @@ class KeyManage:
"message": "encrypt key is empty", "message": "encrypt key is empty",
"status": "failed" "status": "failed"
} }
secrets_encrypt_key = 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)
setattr(current_app, 'secrets_encrypt_key', secrets_encrypt_key) if ok:
setattr(current_app, 'secrets_root_key', root_key) current_app.config["secrets_encrypt_key"] = secrets_encrypt_key
setattr(current_app, 'secrets_shares', []) current_app.config["secrets_root_key"] = root_key
current_app.config["secrets_shares"] = []
return {"message": success, "status": success}
else:
return { return {
"message": success, "message": secrets_encrypt_key,
"status": success "status": "failed"
} }
def unseal(self, key): def unseal(self, key):
@ -152,10 +159,14 @@ class KeyManage:
try: try:
t = [i for i in b64decode(key)] t = [i for i in b64decode(key)]
v = (int("".join([chr(i) for i in t[-2:]])), bytes(t[:-2])) v = (int("".join([chr(i) for i in t[-2:]])), bytes(t[:-2]))
shares = getattr(current_app, "secrets_shares", []) print("............")
# shares = getattr(current_app.config, "secrets_shares", [])
shares = current_app.config.get("secrets_shares", [])
print("222222222222")
if v not in shares: if v not in shares:
shares.append(v) shares.append(v)
setattr(current_app, "secrets_shares", shares) current_app.config["secrets_shares"] = shares
print("shares:", shares)
if len(shares) >= global_key_threshold: if len(shares) >= global_key_threshold:
recovered_secret = Shamir.combine(shares[:global_key_threshold]) recovered_secret = Shamir.combine(shares[:global_key_threshold])
return self.auth_root_secret(b64encode(recovered_secret)) return self.auth_root_secret(b64encode(recovered_secret))
@ -209,10 +220,10 @@ class KeyManage:
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}, False
#
setattr(current_app, 'secrets_root_key', root_key)
setattr(current_app, 'secrets_encrypt_key', encrypt_key)
current_app.config["secrets_root_key"] = root_key
current_app.config["secrets_encrypt_key"] = encrypt_key
print(".....", current_app.config["secrets_root_key"], current_app.config["secrets_encrypt_key"])
self.print_token(shares, root_token=root_key) self.print_token(shares, root_token=root_key)
return {"message": "OK", return {"message": "OK",
@ -271,8 +282,8 @@ class KeyManage:
"status": "failed" "status": "failed"
} }
else: else:
setattr(current_app, 'secrets_root_key', '') current_app.config["secrets_root_key"] = ''
setattr(current_app, 'secrets_encrypt_key', '') current_app.config["secrets_encrypt_key"] = ''
return { return {
"message": success, "message": success,
@ -284,7 +295,7 @@ class KeyManage:
If there is no initialization or the root key is inconsistent, it is considered to be in a sealed state. If there is no initialization or the root key is inconsistent, it is considered to be in a sealed state.
:return: :return:
""" """
secrets_root_key = getattr(current_app, 'secrets_root_key') secrets_root_key = current_app.config.get("secrets_root_key")
root_key = self.backend.get(backend_root_key_name) root_key = self.backend.get(backend_root_key_name)
if root_key == "" or root_key != secrets_root_key: if root_key == "" or root_key != secrets_root_key:
return "invalid root key", True return "invalid root key", True
@ -326,7 +337,8 @@ class KeyManage:
class InnerCrypt: class InnerCrypt:
def __init__(self): def __init__(self):
secrets_encrypt_key = getattr(current_app, 'secrets_encrypt_key', '') secrets_encrypt_key = current_app.config.get("secrets_encrypt_key", "")
print("secrets_encrypt_key:", secrets_encrypt_key)
self.encrypt_key = b64decode(secrets_encrypt_key.encode("utf-8")) self.encrypt_key = b64decode(secrets_encrypt_key.encode("utf-8"))
def encrypt(self, plaintext): def encrypt(self, plaintext):

View File

@ -46,5 +46,4 @@ def register_resources(resource_path, rest_api):
resource_cls.url_prefix = ("",) resource_cls.url_prefix = ("",)
if isinstance(resource_cls.url_prefix, six.string_types): if isinstance(resource_cls.url_prefix, six.string_types):
resource_cls.url_prefix = (resource_cls.url_prefix,) resource_cls.url_prefix = (resource_cls.url_prefix,)
rest_api.add_resource(resource_cls, *resource_cls.url_prefix) rest_api.add_resource(resource_cls, *resource_cls.url_prefix)

View File

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

View File

@ -1,27 +0,0 @@
from api.resource import APIView
from api.models.cmdb import InnerKV
from api.lib.secrets.inner import KeyManage
from flask import request, abort
class InnerSecretUnSealView(APIView):
url_prefix = "/secrets/unseal"
def post(self):
unseal_key = request.headers.get("Inner-Token")
res = KeyManage(InnerKV()).unseal(unseal_key)
if res.get("status") == "failed":
return abort(400, res.get("message"))
return self.jsonify(**res)
class InnerSecretSealView(APIView):
url_prefix = "/secrets/seal"
def post(self):
unseal_key = request.headers.get("Inner-Token")
res = KeyManage(InnerKV()).seal(unseal_key)
if res.get("status") == "failed":
return abort(400, res.get("message"))
return self.jsonify(**res)