vault_to_lockbox_migrator.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426
  1. """
  2. Script to migrate secrets from Hashicorp Vault to Yandex Cloud Lockbox service
  3. command line options
  4. -l --list : dump Vault secrets to screen
  5. -o --outFile [FILENAME] : save Vault secrets to file [file name by default - secrets.json]
  6. -m --migrate : migrate all secrets from Vault to Lockbox
  7. -c --createFrom [FILENAME] : create secrets in Lockbox from file [file name by default - secrets.json]
  8. -d --deleteAll : delete all secrets in Lockbox
  9. To work properly, script need read config values. It's recommended to create .env file in the same directory as the script
  10. with the following content:
  11. VAULT_TOKEN = "00000000-0000-0000-0000-000000000000"
  12. VAULT_URL = "https://localhost:8201"
  13. VAULT_ROOT_PATH = "<your root path of secret store>"
  14. VAULT_KV_VERSION = 2
  15. VAULT_VERIFY_SSL = False
  16. YC_TOKEN = "<insert yc toket (yc iam create token)>"
  17. YANDEX_FOLDER_ID = "<yandex cloud folder where Lockbox service will create your secrets>"
  18. OUT_FILE = "secrets.json"
  19. INPUT_FILE = "secrets.json"
  20. """
  21. import requests
  22. import json
  23. import os
  24. from dotenv import load_dotenv
  25. import urllib.request, ssl, urllib.error
  26. import urllib3
  27. import sys
  28. import getopt
  29. urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
  30. g_vault_token = ""
  31. g_vault_url = ""
  32. g_vault_root_path = ""
  33. g_vault_kv_version = 2
  34. g_vault_verify_ssl = False
  35. g_yandex_token = ""
  36. g_yandex_folder_id = ""
  37. g_yandex_url = "https://lockbox.api.cloud.yandex.net/lockbox/v1/secrets"
  38. g_out_file = "secrets.json"
  39. g_input_file = "secrets.json"
  40. g_secrets = {}
  41. # List Vault keys
  42. def vault_list_keys(root):
  43. url = f'{g_vault_url}/v1/{g_vault_root_path}/metadata/{root}'
  44. # print(f"Vault URL={url}")
  45. if g_vault_verify_ssl:
  46. opener = urllib.request.build_opener(urllib.request.HTTPHandler)
  47. else:
  48. ctx = ssl.create_default_context()
  49. ctx.check_hostname = False
  50. ctx.verify_mode = ssl.CERT_NONE
  51. opener = urllib.request.build_opener(urllib.request.HTTPSHandler(context=ctx), urllib.request.HTTPHandler)
  52. request = urllib.request.Request(url)
  53. request.add_header("X-Vault-Token", g_vault_token)
  54. request.get_method = lambda: 'LIST'
  55. try:
  56. response = opener.open(request)
  57. data = response.read()
  58. data_json = json.loads(data)
  59. # print(data_json["data"]["keys"])
  60. for key in data_json["data"]["keys"]:
  61. if key[-1] == '/':
  62. vault_list_keys(root + key)
  63. else:
  64. vault_get_metadata(root + key)
  65. except urllib.error.HTTPError as err:
  66. print(f'A HTTPError was thrown: {err.code} {err.reason}')
  67. except urllib.error.URLError as err:
  68. print(f'A URLError was thrown: {err=}')
  69. except Exception as err:
  70. print(f"Unexpected {err=}, {type(err)=}")
  71. def vault_get_secrets(path, version, current_version, custom_metadata):
  72. url = f'{g_vault_url}/v1/{g_vault_root_path}/data/{path}?version={version}'
  73. headers = {'X-Vault-Token': g_vault_token}
  74. try:
  75. request = requests.get(url, headers=headers, verify=g_vault_verify_ssl)
  76. key_data = json.loads(request.text)
  77. if path not in g_secrets:
  78. g_secrets[path] = []
  79. key_data['data']['metadata']['current_version'] = current_version
  80. g_secrets[path].append(key_data)
  81. except requests.HTTPError as err:
  82. print(f'A HTTPError was thrown: {err=}')
  83. except Exception as err:
  84. print(f"Unexpected {err=}, {type(err)=}")
  85. def vault_get_metadata(path):
  86. url = f'{g_vault_url}/v1/{g_vault_root_path}/metadata/{path}'
  87. headers = {'X-Vault-Token': g_vault_token}
  88. try:
  89. request = requests.get(url, headers=headers, verify=g_vault_verify_ssl)
  90. for item in request.json()['data']['versions']:
  91. if not request.json()['data']['versions'][item]['destroyed']:
  92. vault_get_secrets(path,
  93. item,
  94. request.json()['data']['current_version'],
  95. request.json()['data']['custom_metadata'])
  96. except requests.HTTPError as err:
  97. print(f'A HTTPError was thrown: {err=}')
  98. except Exception as err:
  99. print(f"Unexpected {err=}, {type(err)=}")
  100. def yandex_prepare_secrets_from_file():
  101. try:
  102. with open(g_input_file) as f:
  103. t_dict = json.load(f)
  104. if t_dict:
  105. for key in t_dict:
  106. for secret in t_dict[key]:
  107. if secret["data"]["metadata"]["version"] == secret["data"]["metadata"]["current_version"]:
  108. yandex_create_secrets(key, secret)
  109. except FileNotFoundError as err:
  110. print(f'Input file "{g_input_file}" is not found.')
  111. except json.JSONDecodeError as err:
  112. print(f'Can not parse input file "{g_input_file}". Check JSON syntax.')
  113. except Exception as err:
  114. print(f"Unexpected {err=}, {type(err)=}")
  115. def yandex_prepare_secrets_from_var():
  116. try:
  117. if g_secrets:
  118. for key in g_secrets:
  119. for secret in g_secrets[key]:
  120. if secret["data"]["metadata"]["version"] == secret["data"]["metadata"]["current_version"]:
  121. yandex_create_secrets(key, secret)
  122. except Exception as err:
  123. print(f"Unexpected {err=}, {type(err)=}")
  124. def yandex_create_secrets(path, secret_json):
  125. url = g_yandex_url
  126. headers = {"Authorization": f"Bearer {g_yandex_token}"}
  127. payload_dict = {}
  128. empty_dict = {}
  129. try:
  130. payload_dict["folderId"] = g_yandex_folder_id
  131. payload_dict["name"] = path
  132. payload_dict["versionDescription"] = ""
  133. payload_dict["description"] = ""
  134. payload_dict["labels"] = empty_dict
  135. payload_dict["kmsKeyId"] = ""
  136. payload_dict["deletionProtection"] = False
  137. payload_dict["versionPayloadEntries"] = yandex_create_secret_payloads(secret_json)
  138. request = requests.post(url, headers=headers, data=json.dumps(payload_dict))
  139. if request.status_code == 200:
  140. print_data = json.loads(request.text)
  141. print(f'Secret {print_data["response"]["name"]} has created with id={print_data["metadata"]["secretId"]}')
  142. else:
  143. print(f'Error. {json.loads(request.text)["message"]}')
  144. except requests.HTTPError as err:
  145. print(f'A HTTPError was thrown: {err}')
  146. except Exception as err:
  147. print(f"Unexpected {err=}, {type(err)=}")
  148. def yandex_create_secret_payloads(secret_dict):
  149. t_arr = []
  150. if len(secret_dict) == 0:
  151. return t_arr
  152. for key in secret_dict["data"]["data"]:
  153. if isinstance(secret_dict["data"]["data"][key], dict):
  154. t_arr.append({"key": "data", "textValue": f'{secret_dict["data"]["data"]}'})
  155. return t_arr
  156. for key in secret_dict["data"]["data"]:
  157. t_arr.append({"key": key, "textValue": secret_dict["data"]["data"][key]})
  158. return t_arr
  159. def yandex_get_secrets():
  160. secret_id = "XXXXX"
  161. url = f"https://lockbox.api.cloud.yandex.net/lockbox/v1/secrets/{secret_id}"
  162. headers = {"Authorization": f"Bearer {g_yandex_token}"}
  163. print(headers)
  164. try:
  165. request = requests.get(url, headers=headers)
  166. print(request.json())
  167. except requests.HTTPError as err:
  168. print(f'A HTTPError was thrown: {err=}')
  169. except Exception as err:
  170. print(f"Unexpected {err=}, {type(err)=}")
  171. def yandex_create_simple_secrets():
  172. # Функция для создания одного секрета с заданными параметрами
  173. headers = {"Authorization": f"Bearer {g_yandex_token}"}
  174. payload_dict = {}
  175. # Если метки не нужны, оставьте этот словарь пустым, это необходимо для правильной работы запроса
  176. # !!! весь текст внутри labels_dict должен быть маленькими буквами и без пробелов
  177. labels_dict = {"label1": "label1_data", "label2": "label2_data"}
  178. t_arr = []
  179. try:
  180. payload_dict["folderId"] = g_yandex_folder_id
  181. payload_dict["name"] = "test"
  182. payload_dict["description"] = ""
  183. payload_dict["labels"] = labels_dict
  184. payload_dict["kmsKeyId"] = ""
  185. payload_dict["versionDescription"] = ""
  186. payload_dict["deletionProtection"] = False
  187. t_arr.append({"key": "FirstKey", "textValue": "password1"})
  188. t_arr.append({"key": "SecondKey", "textValue": "password2"})
  189. payload_dict["versionPayloadEntries"] = t_arr
  190. # можно сохранить в файл для дальнейших тестов с curl
  191. # curl -X POST -d @./lockbox_simple_secret.json -H "Authorization: Bearer <Token>" https://lockbox.api.cloud.yandex.net/lockbox/v1/secrets
  192. # with open("lockbox_simple_secret.json", 'w') as f:
  193. # json.dump(payload_dict, f, indent=4)
  194. print(payload_dict)
  195. request = requests.post(g_yandex_url, headers=headers, data=json.dumps(payload_dict))
  196. request.raise_for_status()
  197. print(request.text)
  198. except requests.HTTPError as err:
  199. print(f'A HTTPError was thrown: {err}')
  200. except Exception as err:
  201. print(f"Unexpected {err=}, {type(err)=}")
  202. def yandex_delete_all_secrets():
  203. # Функция для удаления всех секретов в Lockbox Есть ограничения - по умолчанию происходит запрос 100 секретов за
  204. # один раз, если нужно больше, нужно менять параметры листинга секретов
  205. get_confirmation("This action will delete ALL secrets from Lockbox. Continue?")
  206. headers = {"Authorization": f"Bearer {g_yandex_token}"}
  207. params = {"folderId": g_yandex_folder_id}
  208. update_string = '{"updateMask": "deletionProtection","deletionProtection": false}'
  209. try:
  210. request = requests.get(g_yandex_url, headers=headers, params=params)
  211. if request.status_code == 200:
  212. if len(json.loads(request.text)) > 0:
  213. for item in request.json()["secrets"]:
  214. # Сначала, если есть, убираем запрет на удаление
  215. if item["deletionProtection"]:
  216. print(f'Update delete protection for secretId {item["id"]}')
  217. u_request = requests.patch(f'{g_yandex_url}/{item["id"]}', headers=headers, data=update_string)
  218. u_request.raise_for_status()
  219. print(f'Delete secret with secretId {item["id"]}')
  220. d_request = requests.delete(f'{g_yandex_url}/{item["id"]}', headers=headers)
  221. d_request.raise_for_status()
  222. else:
  223. print(f'There are no secrets in Lockbox service.')
  224. else:
  225. print(f'Error. {json.loads(request.text)["message"]}')
  226. except requests.HTTPError as err:
  227. print(f'A HTTPError was thrown: {err=}')
  228. except Exception as err:
  229. print(f"Unexpected {err=}, {type(err)=}")
  230. def get_confirmation(prompt):
  231. answer = ""
  232. while answer not in ["y", "n"]:
  233. answer = input(f"{prompt} [Y/N]? ").lower()
  234. if answer == "n":
  235. sys.exit(0)
  236. def dump_to_screen():
  237. # List all secrets to screen
  238. vault_list_keys('')
  239. print(json.dumps({**{}, **g_secrets}, indent=2))
  240. def save_to_file():
  241. if os.path.isfile(g_out_file):
  242. get_confirmation(f"File {g_out_file} exist. Overwrite it?")
  243. vault_list_keys('')
  244. t_str = json.dumps(g_secrets, indent=4)
  245. with open(g_out_file, 'w') as f:
  246. print(t_str, file=f)
  247. print(f"File {g_out_file} has created.")
  248. def migrate():
  249. vault_list_keys('')
  250. print(json.dumps({**{}, **g_secrets}, indent=2))
  251. get_confirmation("Need your confirmation to create this secrets in Lockbox service. Continue?")
  252. yandex_prepare_secrets_from_var()
  253. def create_secrets():
  254. if os.path.isfile(g_input_file):
  255. get_confirmation(
  256. f"Need your confirmation to create secrets from file {g_input_file} in Lockbox service. Continue?")
  257. yandex_prepare_secrets_from_file()
  258. else:
  259. print(f"File {g_input_file} is not exist.")
  260. def print_help():
  261. print("Script to migrate secrets from Hashicorp Vault to Yandex Cloud Lockbox service")
  262. print("Command line arguments:")
  263. print("-h : this help")
  264. print("-l or --list : dump Vault secrets to screen")
  265. print("-o or --outFile [FILENAME] : save Vault secrets to file [file name by default - secrets.json]")
  266. print("-m or --migrate : migrate all secrets from Vault to Lockbox")
  267. print("-c or --createFrom [FILENAME] : create secrets in Lockbox from file [file name by default - secrets.json]")
  268. print("-d or --deleteAll : delete all secrets in Lockbox")
  269. def load_config():
  270. global g_vault_token
  271. global g_vault_url
  272. global g_vault_root_path
  273. global g_vault_kv_version
  274. global g_vault_verify_ssl
  275. global g_yandex_token
  276. global g_yandex_folder_id
  277. global g_yandex_url
  278. global g_out_file
  279. global g_input_file
  280. load_dotenv()
  281. exit_flag = False
  282. # print(json.dumps({**{}, **os.environ}, indent=2))
  283. g_vault_token = os.environ.get("VAULT_TOKEN", "")
  284. if len(g_vault_token) == 0:
  285. print("Error. Set VAULT_TOKEN environment variable. For example, export VAULT_TOKEN=$(vault token create).")
  286. exit_flag = True
  287. g_vault_url = os.environ.get("VAULT_URL", "")
  288. if len(g_vault_url) == 0:
  289. print("Error. Set VAULT_URL environment variable. For example, export VAULT_URL=https://localhost:8201")
  290. exit_flag = True
  291. g_vault_root_path = os.environ.get("VAULT_ROOT_PATH", "")
  292. if len(g_vault_root_path) == 0:
  293. print("Error. Set VAULT_ROOT_PATH environment variable. For example, export VAULT_ROOT_PATH=secret")
  294. exit_flag = True
  295. g_yandex_token = os.environ.get("YC_TOKEN", "")
  296. if len(g_yandex_token) == 0:
  297. print("Error. Set YC_TOKEN environment variable. For example, export YC_TOKEN=$(yc iam create-token).")
  298. exit_flag = True
  299. g_yandex_folder_id = os.environ.get("YANDEX_FOLDER_ID", "")
  300. if len(g_yandex_folder_id) == 0:
  301. print("Error. Set YANDEX_FOLDER_ID environment variable. For example, export YANDEX_FOLDER_ID=123456789")
  302. exit_flag = True
  303. g_yandex_url = os.environ.get("YANDEX_URL", "https://lockbox.api.cloud.yandex.net/lockbox/v1/secrets")
  304. g_out_file = os.environ.get("OUT_FILE", "secrets.json")
  305. g_input_file = os.environ.get("INPUT_FILE", "secrets.json")
  306. try:
  307. g_vault_kv_version = int(os.environ.get("VAULT_KV_VERSION", "2"))
  308. if not (g_vault_kv_version == 1 or g_vault_kv_version == 2):
  309. print(f"Possible values of VAULT_KV_VERSION must be 1 or 2")
  310. exit_flag = True
  311. except Exception as err:
  312. print(f"Possible values of VAULT_KV_VERSION must be 1 or 2")
  313. exit_flag = True
  314. test_string = os.environ.get("VAULT_VERIFY_SSL", False)
  315. if test_string == "False":
  316. g_vault_verify_ssl = False
  317. elif test_string == "True":
  318. g_vault_verify_ssl = True
  319. else:
  320. print(f"Possible values of VAULT_VERIFY_SSL must be True or False")
  321. exit_flag = True
  322. if exit_flag:
  323. sys.exit(1)
  324. if __name__ == '__main__':
  325. if len(sys.argv) == 1:
  326. print_help()
  327. sys.exit(1)
  328. try:
  329. opts, args = getopt.getopt(sys.argv[1:], "hlomcd",
  330. ["help", "list", "outFile", "migrate", "createFrom", "deleteAll"])
  331. except getopt.GetoptError:
  332. print_help()
  333. sys.exit(2)
  334. if len(opts) > 1:
  335. print("Specify only one command line argument.")
  336. sys.exit(0)
  337. for opt, arg in opts:
  338. if opt in ("-h", "--help"):
  339. print_help()
  340. sys.exit()
  341. elif opt in ("-l", "--list"):
  342. load_config()
  343. dump_to_screen()
  344. elif opt in ("-o", "--outFile"):
  345. load_config()
  346. if len(sys.argv) > 2:
  347. g_out_file = sys.argv[2]
  348. save_to_file()
  349. elif opt in ("-m", "--migrate"):
  350. load_config()
  351. migrate()
  352. elif opt in ("-c", "--createFrom"):
  353. load_config()
  354. if len(sys.argv) > 2:
  355. g_input_file = sys.argv[2]
  356. create_secrets()
  357. elif opt in ("-d", "--deleteAll"):
  358. load_config()
  359. yandex_delete_all_secrets()
  360. else:
  361. print_help()
  362. sys.exit()