勢(shì)在必行的計(jì)劃狼荞,前期越周全越好宅粥。
一啊楚,升級(jí)原因
1吠冤,安全漏洞
Harbor 官方倉(cāng)庫(kù)公布了 5 個(gè)漏洞,其中包括 2 個(gè)官方定級(jí)為嚴(yán)重的漏洞(CVE-2019-19025恭理、CVE-2019-19023)拯辙,2個(gè)高危級(jí)別漏洞(CVE-2019-19029、CVE-2019-19026)颜价,1個(gè)中等級(jí)別漏洞(CVE-2019-3990)涯保。
- CVE-2019-19025:缺少 CSRF 保護(hù)漏洞,Harbor Web界面未實(shí)現(xiàn)針對(duì)跨站點(diǎn)請(qǐng)求偽造(CSRF)的保護(hù)機(jī)制周伦。通過把經(jīng)過身份驗(yàn)證的用戶吸引到事先準(zhǔn)備好的第三方網(wǎng)站夕春,可導(dǎo)致第三方代表經(jīng)過身份驗(yàn)證的用戶或管理員在平臺(tái)上執(zhí)行任意操作。
- CVE-2019-19023:特權(quán)提升漏洞横辆,該漏洞使普通用戶可以通過API調(diào)用來修改特定用戶的電子郵件地址撇他,從而獲得管理員帳戶特權(quán)。漏洞源于Harbor API沒有對(duì)修改電子郵件地址的API請(qǐng)求進(jìn)行適當(dāng)?shù)臋?quán)限限制狈蚤。
- CVE-2019-19029:通過用戶組進(jìn)行SQL注入困肩,具有項(xiàng)目管理功能的用戶可以利用SQL注入來從底層數(shù)據(jù)庫(kù)讀取機(jī)密信息或進(jìn)行權(quán)限提升。
- CVE-2019-19026:通過項(xiàng)目quotas進(jìn)行SQL注入脆侮,Harbor API的quotas部分存在一個(gè)SQL注入漏洞锌畸。經(jīng)過身份驗(yàn)證的管理員可以通過GET參數(shù)發(fā)送特制的SQL有效負(fù)載,從而從數(shù)據(jù)庫(kù)中提取敏感信息靖避。
- CVE-2019-3990:用戶枚舉漏洞潭枣,該漏洞存在于 “/users” api 中比默,這個(gè)功能應(yīng)該僅限于管理員使用,可是該限制可被繞過盆犁,非管理員用戶(例如通過自我注冊(cè)創(chuàng)建的用戶)可以通過向 /api/users/search發(fā)送 GET請(qǐng)求來列出所有用戶名和用戶ID命咐、確認(rèn)與用戶名關(guān)聯(lián)的電子郵件地址等。
2谐岁,功能升級(jí)
Harbor 的 OCI Artifact 功能醋奠,可以用來存儲(chǔ)、分發(fā)和管理機(jī)器學(xué)習(xí)的模型文件伊佃。
二窜司,升級(jí)流程
三,升級(jí)步驟
先按v1版的harbor配置航揉,建一個(gè)v2版的harbor(掛載大存儲(chǔ)塞祈,ldap用戶認(rèn)證等)。
1,導(dǎo)出v1版本的項(xiàng)目和鏡像列表
運(yùn)行harbor_v1_images_export.py腳本帅涂,從V1版本harbor中议薪,提取所有的項(xiàng)目列表和鏡像文件列表。
為支持多次運(yùn)行此腳本漠秋,會(huì)導(dǎo)入之前運(yùn)行結(jié)果笙蒙,以縮短再次運(yùn)行的時(shí)間抵屿。
其中庆锦,項(xiàng)目列表為pro.csv文件,鏡像列表為repo_v1.csv文件
"""
用于將老版本的harbor中的Project和repo鏡像提取出來轧葛,保存到文件中搂抒。
admin
2020-11-24
"""
import requests
# 常量定義
harbor_domain_v1 = 'harbor.demo.cn'
username = 'admin'
password = 'xxxx'
pro_file = 'pro.csv'
repo_file = 'repo_v1.csv'
# V1版本,請(qǐng)求API使用session尿扯。
class RequestClient:
def __init__(self, login_url, username, password):
self.login_url = login_url
self.username = username
self.password = password
self.session = requests.Session()
self.login()
def login(self):
self.session.post(self.login_url,
params={'principal': self.username,
'password': self.password})
print('login', self.session)
# 將Harbor常用操作包裝成一個(gè)class
class HarborReposV1:
def __init__(self, harbor_domain, username, password, schema='http'):
self.schema = schema
self.harbor_domain = harbor_domain
self.harbor_url = self.schema + '://' + harbor_domain
self.harbor_login_url = self.harbor_url + '/login'
self.harbor_api_url = self.harbor_url + '/api'
self.harbor_pro_url = self.harbor_api_url + '/projects'
self.harbor_repos_url = self.harbor_api_url + '/repositories'
self.username = username
self.password = password
# self.client和self.pros_obj在初始化時(shí)就生成好求晶,使用起來更流暢
self.client = RequestClient(self.harbor_login_url,
self.username,
self.password)
self.pros_obj = self.__fetch_pros_obj()
def __fetch_pros_obj(self):
return self.client.session.get(self.harbor_pro_url).json()
# 獲取所有的project id
def fetch_pros_id(self):
pros_id = list()
for i in self.pros_obj:
pros_id.append(i['project_id'])
return pros_id
# 根據(jù)project id獲取project名稱及public屬性
def fetch_pro_name(self, pro_id):
for i in self.pros_obj:
if i['project_id'] == pro_id:
pro_name = i['name']
pro_public = i['metadata']['public']
return pro_name, pro_public
# 根據(jù)project id獲取此project下所有鏡像名稱
def fetch_repos_name(self, pro_id):
repos_name = list()
repos_res = self.client.session.get(self.harbor_repos_url,
params={'project_id': pro_id})
for repo in repos_res.json():
repos_name.append(repo['name'])
return repos_name
# 根據(jù)鏡像名稱,獲取此鏡像的所有tag
def fetch_repos(self, repo_name):
repos = list()
harbor_tag_url = self.harbor_repos_url + '/' + repo_name + '/tags'
repos_res = self.client.session.get(harbor_tag_url)
for tag in repos_res.json():
full_repo_name = '{}/{}:{}'.format(self.harbor_domain, repo_name, tag['name'])
repos.append(full_repo_name)
return repos
# 將文件內(nèi)的所有行讀取成列表
def read_file_list(file_name):
with open(file_name, 'r') as f_r:
return [line.strip() for line in f_r]
# 將指定內(nèi)容追加到指定文件
def insert_file(file_name, content):
# 對(duì)于python3, 第三個(gè)參數(shù)衷笋,指定編碼方式必須要加encoding=“utf-8”
with open(file_name, 'a', encoding='utf-8') as f_w:
print(content)
f_w.write('{}\r\n'.format(content))
print("保存{}成功".format(content))
if __name__ == '__main__':
# 初始化harbor連接數(shù)據(jù)
res_v1 = HarborReposV1(harbor_domain_v1, username, password)
# 如果文件中已存在項(xiàng)目和鏡像芳杏,則先提取到列表里,避免重復(fù)插入辟宗,這樣腳本就可以多次運(yùn)行遷移
try:
pro_list = read_file_list(pro_file)
repo_list = read_file_list(repo_file)
except FileNotFoundError: # 文件不能找到的異常處理
pro_list = []
repo_list = []
print("首次運(yùn)行爵赵,還沒有文件,繼續(xù)處理泊脐。空幻。.")
# 獲取所有project id
for pro_id in res_v1.fetch_pros_id():
pro_name, is_public = res_v1.fetch_pro_name(pro_id)
line_str = '{},{}'.format(pro_name, is_public)
# 只將新的項(xiàng)目加入文件
if line_str not in pro_list:
insert_file(pro_file, line_str)
else:
print('project{}已存在于文件{}中'.format(line_str, pro_file))
# 獲取到所有鏡像名稱
repos_name = res_v1.fetch_repos_name(pro_id=pro_id)
for repo_name in repos_name:
# 獲取到鏡像的所有tag
repos = res_v1.fetch_repos(repo_name=repo_name)
for full_repo_name in repos:
# 只將新的鏡像tag加入文件
if full_repo_name not in repo_list:
insert_file(repo_file, full_repo_name)
else:
print('鏡像tag{}已存在于文件{}中'.format(full_repo_name, repo_file))
2,在v2版中導(dǎo)入項(xiàng)目列表
運(yùn)行harbor_v2_projects_create.py腳本,將pro.csv文件中的項(xiàng)目列表導(dǎo)入新版harbor中容客,其中秕铛,保留了每個(gè)項(xiàng)目的public屬性(true or false)约郁。
"""
將從將倉(cāng)庫(kù)里獲取到的所有project,在新倉(cāng)庫(kù)中重建好但两,帶public屬性的
admin
2020-11-24
"""
import requests
from requests.auth import HTTPBasicAuth
import json
pro_file = 'pro.csv'
harbor_domain_v2 = 'harbor-test.demo.cn:8086'
v2_username = 'admin'
v2_password = 'xxxxx'
# Harbor V2版本的class
class HarborReposV2:
def __init__(self, harbor_domain, username, password, schema='http'):
self.schema = schema
self.harbor_domain = harbor_domain
self.harbor_url = self.schema + '://' + harbor_domain
# 新版harbor 2版本的api地址
self.harbor_api_url = self.harbor_url + '/api/v2.0'
self.harbor_pro_url = self.harbor_api_url + '/projects'
self.username = username
self.password = password
# 使用HTTPBasicAuth認(rèn)證鬓梅,這也是避免那些CSRF Token Invalid的最佳辦法
# 是從harbor API里看認(rèn)證方式獲得的啟發(fā)。
self.auth = HTTPBasicAuth(self.username, self.password)
# 好像只要三個(gè)要素谨湘,就可以新建一個(gè)project了己肮。細(xì)節(jié)待完善。
def create_pros(self, pro_name, is_public):
pro_obj = dict()
pro_obj['project_name'] = pro_name
pro_obj["metadata"] = dict()
pro_obj["metadata"]["public"] = is_public
# pro_obj["metadata"]["enable_content_trust"] = i["enable_content_trust"]
# pro_obj["metadata"]["prevent_vul"] = i["prevent_vulnerable_images_from_running"]
# pro_obj["metadata"]["severity"] = i["prevent_vulnerable_images_from_running_severity"]
# pro_obj["metadata"]["auto_scan"] = i["automatically_scan_images_on_push"]
headers = {"content-type": "application/json"}
res = requests.post(self.harbor_pro_url,
auth=self.auth,
headers=headers,
data=json.dumps(pro_obj))
if res.status_code == 409:
print("\033[32m 項(xiàng)目 %s 已經(jīng)存在!\033[0m" % pro_name)
return True
elif res.status_code == 201:
# print(res.status_code)
print("\033[33m 創(chuàng)建項(xiàng)目%s成功!\033[0m" % pro_name)
return True
else:
print("\033[35m 創(chuàng)建項(xiàng)目%s失敗!\033[0m" % pro_name)
return False
# 從舊倉(cāng)庫(kù)導(dǎo)出來的projects列表悲关,讀取出來
def read_file_list(file_name):
with open(file_name, 'r') as f_r:
return [line.strip() for line in f_r]
if __name__ == '__main__':
# 初始化
res_v2 = HarborReposV2(harbor_domain_v2, v2_username, v2_password)
pro_list = read_file_list(pro_file)
for item in pro_list:
pro_name, is_public = item.split(',')
res_v2.create_pros(pro_name, is_public)
3谎僻,在v2版中導(dǎo)入鏡像列表
運(yùn)行harbor_v2_images_import.py腳本,將repo_v1.csv文件中的鏡像列表導(dǎo)入新版harbor中寓辱,同時(shí)艘绍,生成repo_v2.csv作為校驗(yàn)文件,以支持多次運(yùn)行此腳本秫筏。
為了讓中間的遷移機(jī)器不至于容量爆掉诱鞠,在每遷移完一個(gè)鏡像的所有tag之后,會(huì)刪除此鏡像的所有文件(每個(gè)鏡像的所有tag这敬,會(huì)共用基礎(chǔ)層航夺,如果導(dǎo)入一個(gè)鏡像就刪除一個(gè)鏡像,效率會(huì)很慢崔涂,且重復(fù)傳輸嚴(yán)重阳掐,想你一個(gè)有300個(gè)tag的鏡像)。
"""
用于保存到文件中鏡像遷移到新的harbor鏡像倉(cāng)庫(kù)當(dāng)中冷蚂。
admin
2020-11-24
"""
import subprocess
# 定義常量
repo_file = 'repo_v1.csv'
new_repo_file = 'repo_v2.csv'
harbor_domain_v1 = 'harbor.demo.cn'
v1_username = 'admin'
v1_password = 'xxxx'
harbor_domain_v2 = 'harbor-test.demo.cn:8086'
v2_username = 'admin'
v2_password = 'xxxx'
repo_list = list()
repo_dict = dict()
new_repo_list = list()
def read_file_list(file_name):
with open(file_name, 'r') as f_r:
return [line.strip() for line in f_r]
def insert_file(file_name, content):
# 對(duì)于python3, 第三個(gè)參數(shù)缭保,指定編碼方式必須要加encoding=“utf-8”
with open(file_name, 'a', encoding='utf-8') as f_w:
print(content)
f_w.write('{}\r\n'.format(content))
print("保存{}成功".format(content))
# 從舊版的harbor中pull鏡像,tag更名之后蝙茶,push到新倉(cāng)庫(kù)艺骂,記得先登陸
def migrate_repos(v1_repo_name, v2_repo_name):
cmd_list = []
old_repo_login = "docker login {} -u {} -p {}".format(harbor_domain_v1, v1_username, v1_password)
pull_old_repo = "docker pull " + v1_repo_name
tag_repo = "docker tag " + v1_repo_name + " " + v2_repo_name
new_repo_login = "docker login {} -u {} -p {}".format(harbor_domain_v2, v2_username, v2_password)
push_new_repo = "docker push " + v2_repo_name
cmd_list.append(old_repo_login)
cmd_list.append(pull_old_repo)
cmd_list.append(tag_repo)
cmd_list.append(new_repo_login)
cmd_list.append(push_new_repo)
ret_sum = 0
for cmd in cmd_list:
print("\033[34m Current command: %s\033[0m" % cmd)
ret = subprocess.call(cmd, shell=True)
ret_sum += ret
if ret_sum == 0:
print("\033[32m migrate %s success!\033[0m" % v2_repo_name)
insert_file(new_repo_file, v2_repo_name)
return True
else:
print("\033[33m migrate %s faild!\033[0m" % v2_repo_name)
return False
# 當(dāng)一個(gè)鏡像的所有tag遷移完成之后,清除此鏡像所有tag,挪出空間隆夯,不然會(huì)爆掉
def delete_local_repos(repo, tags):
for tag in tags:
repo_tag = '{}:{}'.format(repo, tag)
cmd = 'docker rmi {}'.format(repo_tag)
if subprocess.call(cmd, shell=True) == 0:
print("\033[32m 刪除 {} 成功!\033[0m".format(repo_tag))
else:
print("\033[32m 刪除 {} 失敗钳恕,繼續(xù)執(zhí)行!\033[0m".format(repo_tag))
if __name__ == '__main__':
try:
new_repo_list = read_file_list(new_repo_file)
except FileNotFoundError:
print('首次導(dǎo)入。還沒有文件蹄衷。')
# 這里的騷操作忧额,是為了能讓同一個(gè)鏡像的不同tag,作同一批次的pull和push宦芦,
# 操作完之后宙址,才作docker image rmi的操作,肯定會(huì)顯著縮短時(shí)間调卑,
# 因?yàn)橥粋€(gè)鏡像的不同tag抡砂,很多層是相同的
# 字典的鍵為鏡像名,值為tag列表
for item in read_file_list(repo_file):
repo, tag = item.split(':')
if repo not in repo_dict:
repo_dict[repo] = [tag]
else:
repo_dict[repo].append(tag)
# 遍歷這個(gè)字典大咱,作遷移
for item in repo_dict.items():
repo, tags = item
# 新的倉(cāng)庫(kù)的鏡像地址,需要整合新倉(cāng)庫(kù)的地址及舊倉(cāng)庫(kù)的項(xiàng)目repo名稱
repo_replace = repo.split('/')
repo_replace[0] = harbor_domain_v2
v2_repo = '/'.join(repo_replace)
# demo小劑量測(cè)試
if 'nginx-ingress-controller' in repo:
for tag in tags:
v1_repo_name = '{}:{}'.format(repo, tag)
v2_repo_name = '{}:{}'.format(v2_repo, tag)
# print(v1_repo_name)
# print(v2_repo_name)
# 已導(dǎo)入過的注益,忽略,減少時(shí)間
if v2_repo_name in new_repo_list:
print('{}已導(dǎo)入新harbor倉(cāng)庫(kù)'.format(v2_repo_name))
continue
# 真正的導(dǎo)出導(dǎo)入操作
if not migrate_repos(v1_repo_name, v2_repo_name):
print('導(dǎo)入失敗')
break
else:
print('導(dǎo)入{}成功'.format(v2_repo_name))
# 這里使用for...else...配合continue和break丑搔,可以直接跳出兩個(gè)for循環(huán)外面
else:
print('{}導(dǎo)入完成厦瓢,清除此鏡像的所有tag'.format(repo))
# 一個(gè)repo導(dǎo)入完成,清除新舊倉(cāng)庫(kù)的所有tag啤月。
delete_local_repos(repo, tags)
delete_local_repos(v2_repo, tags)
continue
break
4煮仇,DNS切換
DNS切換,將指到v1版harbor的域名谎仲,指向v1版的harbor浙垫。
5,更新V2版配置
新版harbor更改配置郑诺,提供與域名一致的服務(wù)夹姥。
Harbor.yml
重啟harbor,使配置生效
docker-comppose down
./prepare
docker-compose up -d
(同時(shí)辙诞,v1版harbor更改為另外的域名或ip辙售,不急馬下線,待v2版穩(wěn)定后下線飞涂,有個(gè)別鏡像旦部,還可以手工導(dǎo)入)
6,測(cè)試驗(yàn)證
在k8s環(huán)境封拧,或是docker環(huán)境下志鹃,測(cè)試是否已平滑升級(jí)完成夭问。
四泽西,此種升級(jí)方案的優(yōu)勢(shì)和注意要點(diǎn)
在標(biāo)準(zhǔn)推薦的harbor升級(jí)方案中,從1.5到2.1缰趋,會(huì)涉及數(shù)據(jù)庫(kù)的轉(zhuǎn)換(從mysql轉(zhuǎn)postgresql)捧杉。而我公司安裝的harbor,是用的docker-compose方案秘血,全docker部署味抖,無形中增加了升級(jí)難度。
在我們?cè)O(shè)計(jì)的這個(gè)升級(jí)方案中灰粮,如果在DNS切換后仔涩,測(cè)試失敗,是可以作回滾的粘舟,只要DNS切回即可熔脂。
另外佩研,它也支持?jǐn)帱c(diǎn)持續(xù)升級(jí)。也就是在空間和時(shí)間許可的情況下霞揉,分多個(gè)批次旬薯,來將V1版的鏡像遷移到V2版中。而在DNS切換的這個(gè)維護(hù)時(shí)間窗口內(nèi)适秩,只需要遷移極少的鏡像绊序,花極少的時(shí)間來作最后的升級(jí)。而無須在短短一天之內(nèi)秽荞,遷移上T的數(shù)據(jù)骤公。
參考URL:
http://blog.nsfocus.net/cve-2019-19025-cve-2019-19023-cve-2019-19029-cve-2019-19026-cve-2019-3990/
https://www.cnblogs.com/breezey/p/10615242.html