feat: secrets

This commit is contained in:
pycook 2023-10-27 17:42:49 +08:00
parent 6fff2fe9df
commit 27a1c75a25
7 changed files with 98 additions and 60 deletions

View File

@ -26,14 +26,14 @@ Flask-Bcrypt = "==1.0.1"
Flask-Cors = ">=3.0.8" Flask-Cors = ">=3.0.8"
ldap3 = "==2.9.1" ldap3 = "==2.9.1"
pycryptodome = "==3.12.0" pycryptodome = "==3.12.0"
cryptography = "==41.0.2" cryptography = ">=41.0.2"
# Caching # Caching
Flask-Caching = ">=1.0.0" Flask-Caching = ">=1.0.0"
# Environment variable parsing # Environment variable parsing
environs = "==4.2.0" environs = "==4.2.0"
marshmallow = "==2.20.2" marshmallow = "==2.20.2"
# async tasks # async tasks
celery = "==5.3.1" celery = ">=5.3.1"
celery_once = "==3.0.1" celery_once = "==3.0.1"
more-itertools = "==5.0.0" more-itertools = "==5.0.0"
kombu = "==5.3.1" kombu = "==5.3.1"

View File

@ -29,10 +29,9 @@ from api.lib.perm.acl.resource import ResourceCRUD
from api.lib.perm.acl.resource import ResourceTypeCRUD from api.lib.perm.acl.resource import ResourceTypeCRUD
from api.lib.perm.acl.role import RoleCRUD from api.lib.perm.acl.role import RoleCRUD
from api.lib.perm.acl.user import UserCRUD from api.lib.perm.acl.user import UserCRUD
from api.lib.secrets.inner import KeyMange from api.lib.secrets.inner import KeyManage
from api.lib.secrets.secrets import InnerKVManger
from api.lib.secrets.inner import global_key_threshold from api.lib.secrets.inner import global_key_threshold
from api.lib.secrets.secrets import InnerKVManger
from api.models.acl import App from api.models.acl import App
from api.models.acl import ResourceType from api.models.acl import ResourceType
from api.models.cmdb import Attribute from api.models.cmdb import Attribute
@ -57,6 +56,7 @@ def cmdb_init_cache():
if relations: if relations:
rd.create_or_update(relations, REDIS_PREFIX_CI_RELATION) rd.create_or_update(relations, REDIS_PREFIX_CI_RELATION)
es = None
if current_app.config.get("USE_ES"): if current_app.config.get("USE_ES"):
from api.extensions import es from api.extensions import es
from api.models.cmdb import Attribute from api.models.cmdb import Attribute
@ -323,25 +323,20 @@ def cmdb_inner_secrets_init():
""" """
init inner secrets for password feature init inner secrets for password feature
""" """
KeyMange(backend=InnerKVManger).init() KeyManage(backend=InnerKVManger).init()
@click.command() @click.command()
@click.option(
'-k',
'--token',
help='root token',
)
@with_appcontext @with_appcontext
def cmdb_inner_secrets_unseal(token): def cmdb_inner_secrets_unseal():
""" """
unseal the secrets feature unseal the secrets feature
""" """
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 token {i + 1}', hide_input=True, confirmation_prompt=False)
assert token is not None assert token is not None
res = KeyMange(backend=InnerKVManger).unseal(token) res = KeyManage(backend=InnerKVManger).unseal(token)
KeyMange.print_response(res) KeyManage.print_response(res)
@click.command() @click.command()
@ -358,8 +353,8 @@ def cmdb_inner_secrets_seal(token):
seal the secrets feature seal the secrets feature
""" """
assert token is not None assert token is not None
res = KeyMange(backend=InnerKVManger()).seal(token) res = KeyManage(backend=InnerKVManger()).seal(token)
KeyMange.print_response(res) KeyManage.print_response(res)
@click.command() @click.command()
@ -368,7 +363,49 @@ def cmdb_inner_secrets_auto_seal():
""" """
auto seal the secrets feature auto seal the secrets feature
""" """
res = KeyMange(current_app.config.get("INNER_TRIGGER_TOKEN"), backend=InnerKVManger()).auto_unseal() res = KeyManage(current_app.config.get("INNER_TRIGGER_TOKEN"), backend=InnerKVManger()).auto_unseal()
KeyMange.print_response(res) KeyManage.print_response(res)
@click.command()
@with_appcontext
def cmdb_password_data_migrate():
"""
Migrate CI password data, version >= v2.3.6
"""
from api.models.cmdb import CIIndexValueText
from api.models.cmdb import CIValueText
from api.lib.secrets.inner import InnerCrypt
from api.lib.secrets.vault import VaultClient
attrs = Attribute.get_by(to_dict=False)
for attr in attrs:
if attr.is_password:
value_table = CIIndexValueText if attr.is_index else CIValueText
for i in value_table.get_by(attr_id=attr.id, to_dict=False):
if current_app.config.get("SECRETS_ENGINE", 'inner') == 'inner':
_, status = InnerCrypt().decrypt(i.value)
if status:
continue
encrypt_value, status = InnerCrypt().encrypt(i.value)
if status:
CIValueText.create(ci_id=i.ci_id, attr_id=attr.id, value=encrypt_value)
else:
continue
elif current_app.config.get("SECRETS_ENGINE") == 'vault':
if i.value == '******':
continue
vault = VaultClient(current_app.config.get('VAULT_URL'), current_app.config.get('VAULT_TOKEN'))
try:
vault.update("/{}/{}".format(i.ci_id, i.attr_id), dict(v=i.value))
except Exception as e:
print('save password to vault failed: {}'.format(e))
continue
else:
continue
i.delete()

View File

@ -7,6 +7,7 @@ QUERY_CIS_BY_VALUE_TABLE = """
attr.alias AS attr_alias, attr.alias AS attr_alias,
attr.value_type, attr.value_type,
attr.is_list, attr.is_list,
attr.is_password,
c_cis.type_id, c_cis.type_id,
{0}.ci_id, {0}.ci_id,
{0}.attr_id, {0}.attr_id,
@ -26,7 +27,8 @@ QUERY_CIS_BY_IDS = """
A.attr_alias, A.attr_alias,
A.value, A.value,
A.value_type, A.value_type,
A.is_list A.is_list,
A.is_password
FROM FROM
({1}) AS A {0} ({1}) AS A {0}
ORDER BY A.ci_id; ORDER BY A.ci_id;
@ -43,7 +45,7 @@ FACET_QUERY1 = """
FACET_QUERY = """ FACET_QUERY = """
SELECT {0}.value, SELECT {0}.value,
count({0}.ci_id) count(distinct {0}.ci_id)
FROM {0} FROM {0}
INNER JOIN ({1}) AS F ON F.ci_id={0}.ci_id INNER JOIN ({1}) AS F ON F.ci_id={0}.ci_id
WHERE {0}.attr_id={2:d} WHERE {0}.attr_id={2:d}

View File

@ -1,25 +1,24 @@
import os
import secrets
import sys
from base64 import b64encode, b64decode from base64 import b64encode, b64decode
from Cryptodome.Protocol.SecretSharing import Shamir
from colorama import Back from colorama import Back
from colorama import Fore from colorama import Fore
from colorama import Style from colorama import Style
from colorama import init as colorama_init from colorama import init as colorama_init
from cryptography.hazmat.backends import default_backend from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives import padding
from cryptography.hazmat.primitives.ciphers import Cipher from cryptography.hazmat.primitives.ciphers import Cipher
from cryptography.hazmat.primitives.ciphers import algorithms from cryptography.hazmat.primitives.ciphers import algorithms
from cryptography.hazmat.primitives.ciphers import modes 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 import hashes
from cryptography.hazmat.primitives import padding
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from flask import current_app
import os
import secrets
import sys
from Cryptodome.Protocol.SecretSharing import Shamir
# global_root_key just for test here # global_root_key just for test here
global_root_key = ""
global_encrypt_key = ""
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
global_key_threshold = 3 # Minimum number of shares required to rebuild the key global_key_threshold = 3 # Minimum number of shares required to rebuild the key
@ -44,7 +43,7 @@ def string_to_bytes(value):
return byte_string return byte_string
class cache_backend: class CacheBackend:
def __init__(self): def __init__(self):
pass pass
@ -62,7 +61,7 @@ class cache_backend:
class Backend: class Backend:
def __init__(self, backend=None): def __init__(self, backend=None):
if not backend: if not backend:
self.backend = cache_backend self.backend = CacheBackend
else: else:
self.backend = backend self.backend = backend
@ -73,14 +72,13 @@ class Backend:
return self.backend.add(key, value) return self.backend.add(key, value)
class KeyMange: class KeyManage:
def __init__(self, trigger=None, backend=None): def __init__(self, trigger=None, backend=None):
self.trigger = trigger self.trigger = trigger
self.backend = backend self.backend = backend
if backend: if backend:
self.backend = Backend(backend) self.backend = Backend(backend)
pass
def hash_root_key(self, value): def hash_root_key(self, value):
algorithm = hashes.SHA256() algorithm = hashes.SHA256()
@ -136,7 +134,6 @@ class KeyMange:
"status": "failed" "status": "failed"
} }
backend_root_key_hash = self.backend.get(backend_root_key_name) backend_root_key_hash = self.backend.get(backend_root_key_name)
print(root_key, root_key_hash, backend_root_key_hash)
if not backend_root_key_hash: if not backend_root_key_hash:
return { return {
"message": "should init firstly", "message": "should init firstly",
@ -153,12 +150,11 @@ class KeyMange:
"message": "encrypt key is empty", "message": "encrypt key is empty",
"status": "failed" "status": "failed"
} }
global global_encrypt_key secrets_encrypt_key = InnerCrypt.aes_decrypt(string_to_bytes(root_key), encrypt_key_aes)
global global_root_key setattr(current_app, 'secrets_encrypt_key', secrets_encrypt_key)
global global_shares setattr(current_app, 'secrets_root_key', root_key)
global_encrypt_key = InnerCrypt.aes_decrypt(string_to_bytes(root_key), encrypt_key_aes) setattr(current_app, 'secrets_shares', [])
global_root_key = root_key
global_shares = []
return { return {
"message": success, "message": success,
"status": success "status": success
@ -170,7 +166,7 @@ class KeyMange:
"message": "current status is unseal, skip", "message": "current status is unseal, skip",
"status": "skip" "status": "skip"
} }
global global_shares, global_root_key, global_encrypt_key global global_shares
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]))
@ -197,6 +193,7 @@ class KeyMange:
return "already exist", [], False return "already exist", [], False
secret = AESGCM.generate_key(128) secret = AESGCM.generate_key(128)
shares = self.generate_keys(secret) shares = self.generate_keys(secret)
return b64encode(secret), shares, True return b64encode(secret), shares, True
def init(self): def init(self):
@ -229,10 +226,11 @@ class KeyMange:
if not ok: if not ok:
return {"message": msg}, False return {"message": msg}, False
# #
global global_root_key, global_encrypt_key setattr(current_app, 'secrets_root_key', root_key)
global_root_key = root_key setattr(current_app, 'secrets_encrypt_key', encrypt_key)
global_encrypt_key = encrypt_key
self.print_token(shares, root_token=root_key) self.print_token(shares, root_token=root_key)
return {"message": "OK", return {"message": "OK",
"details": { "details": {
"root_token": root_key, "root_token": root_key,
@ -289,10 +287,9 @@ class KeyMange:
"status": "failed" "status": "failed"
} }
else: else:
global global_root_key setattr(current_app, 'secrets_root_key', '')
global global_encrypt_key setattr(current_app, 'secrets_encrypt_key', '')
global_root_key = ""
global_encrypt_key = ""
return { return {
"message": success, "message": success,
"status": success "status": success
@ -303,9 +300,11 @@ class KeyMange:
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')
root_key = self.backend.get(backend_root_key_name) root_key = self.backend.get(backend_root_key_name)
if root_key == "" or root_key != global_root_key: if root_key == "" or root_key != secrets_root_key:
return "invalid root key", True return "invalid root key", True
return "", False return "", False
@classmethod @classmethod
@ -340,9 +339,11 @@ class KeyMange:
else: else:
print(Fore.GREEN, message) print(Fore.GREEN, message)
class InnerCrypt: class InnerCrypt:
def __init__(self, trigger=None): def __init__(self):
self.encrypt_key = b64decode(global_encrypt_key.encode("utf-8")) secrets_encrypt_key = getattr(current_app, 'secrets_encrypt_key', '')
self.encrypt_key = b64decode(secrets_encrypt_key.encode("utf-8"))
def encrypt(self, plaintext): def encrypt(self, plaintext):
""" """
@ -389,8 +390,7 @@ class InnerCrypt:
if __name__ == "__main__": if __name__ == "__main__":
print(global_encrypt_key) km = KeyManage()
km = KeyMange()
# info, shares, status = km.generate_unseal_keys() # info, shares, status = km.generate_unseal_keys()
# print(info, shares, status) # print(info, shares, status)
# print("..................") # print("..................")
@ -415,4 +415,3 @@ if __name__ == "__main__":
print("Ciphertext:", t_ciphertext) print("Ciphertext:", t_ciphertext)
decrypted_plaintext, status2 = c.decrypt(t_ciphertext) decrypted_plaintext, status2 = c.decrypt(t_ciphertext)
print("Decrypted plaintext:", decrypted_plaintext) print("Decrypted plaintext:", decrypted_plaintext)
print(global_encrypt_key)

View File

@ -136,6 +136,6 @@ if __name__ == "__main__":
# sdk.enable_secrets_engine() # sdk.enable_secrets_engine()
_data = {"key1": "value1", "key2": "value2", "key3": "value3"} _data = {"key1": "value1", "key2": "value2", "key3": "value3"}
_data = sdk.update(_path, _data, overwrite=True, encrypt=True) _data = sdk.update(_path, _data, overwrite=True, encrypt=True)
print(_data.status_code) print(_data)
_data = sdk.read(_path, decrypt=True) _data = sdk.read(_path, decrypt=True)
print(_data) print(_data)

View File

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

View File

@ -1,7 +1,7 @@
-i https://mirrors.aliyun.com/pypi/simple -i https://mirrors.aliyun.com/pypi/simple
alembic==1.7.7 alembic==1.7.7
bs4==0.0.1 bs4==0.0.1
celery==5.3.1 celery>=5.3.1
celery-once==3.0.1 celery-once==3.0.1
click==8.1.3 click==8.1.3
elasticsearch==7.17.9 elasticsearch==7.17.9