TLS證書在當今互聯網中處於最重要的一環,他保護著我們的隱私數據的安全,也是最流行的加密方式之一。所以TLS證書的快速部署對於小而美的應用能讓其快速的落地使用。 ...
wmproxy
wmproxy
已用Rust
實現http/https
代理, socks5
代理, 反向代理, 負載均衡, 靜態文件伺服器,websocket
代理,四層TCP/UDP轉發,內網穿透等,會將實現過程分享出來,感興趣的可以一起造個輪子
項目地址
國內: https://gitee.com/tickbh/wmproxy
github: https://github.com/tickbh/wmproxy
設計目標
讓系統擁有acme的能力,即可以領取Let's Encrypt
的證書簽發,快速實現上線部署。
acme是什麼?
ACME(Automated Certificate Management Environment)
是一個用於自動化管理SSL/TLS證書的協議。它通過自動獲取、自動更新和自動拒絕等功能,可以大大提高SSL證書的管理和更新效率,降低錯誤風險,提高網站的安全性和穩定性。
當ACME伺服器發佈不安全的SSL證書時,可以通過ACME協議自動拒絕證書,確保網站始終使用安全的SSL證書。此外,ACME協議還支持自動續期功能,這意味著在證書到期之前,系統可以自動申請並獲取新的證書,從而避免了因證書過期而導致的網站訪問中斷或安全風險。
acme的定義
acme是一個可以自動獲取 TLS證書的協議,acmev1已經被正式棄用,現行的acme在rfc8555定義。其中定義了SSL如何獲取的整個過程,包括其中最重要的許可權鑒定。
以下是兩種acme判定許可權擁有者的鑒權方式,以下是wmproxy.net
做為功能變數名稱來舉例。
HTTP-01 方式鑒定
HTTP-01 的校驗原理是訪問給你功能變數名稱指向的 HTTP 服務增加一個臨時 location,Let’s Encrypt
會發送 http 請求到 http://wmproxy.net/.well-known/acme-challenge/
,wmproxy.net
就是被校驗的功能變數名稱,TOKEN 是 ACME 協議的客戶端負責放置的文件,在這裡 ACME 客戶端就是 acme-lib
。Let’s Encrypt 會對比 TOKEN 是否符合預期,校驗成功後就會頒發證書。不支持泛功能變數名稱證書。成功後我們就可以擁有TLS證書了。
-
優點
配置簡單通用
任何DNS服務商均可 -
缺點
需要依賴HTTP伺服器
集群會無法申請的可能
不支持泛功能變數名稱
DNS-01 方式鑒定
在 ACME DNS 質詢驗證的自動化中,以下是一些關鍵步驟:
- 生成一個 DNS TXT 記錄,如
_acme-challenge
。 - 將 TXT 記錄添加到 DNS 區域中。
- 通知 Let's Encrypt 驗證 DNS 記錄。
- 等待 Let's Encrypt 驗證完成。
- 如果驗證成功,則生成證書。
- 刪除 DNS TXT 記錄。
此方法不需要你的服務使用 Http 服務,並且支持泛功能變數名稱證書。
-
優點
不需要HTTP伺服器
支持泛功能變數名稱 -
缺點
各DNS服務商均不一致
acme在保證安全的情況下縮短了TLS證書的申請流程,可以自動化的進行部署,極大的緩解因證書過期帶來的麻煩。
代碼實現
依賴:acme-lib
改造:之前是確定配置證書及密鑰後直接生成完整的TLS信息TlsAcceptor
,那麼現在在未申請到證書前,不能確定完整的TlsAcceptor
,需要對初始化對象進行重新改造處理。
源碼:wrap_tls_accepter
定義:
/// 為了適應acme, 重新改造Acceptor進行封裝處理
#[derive(Clone)]
pub struct WrapTlsAccepter {
pub last: Instant,
pub domain: Option<String>,
pub accepter: Option<TlsAcceptor>,
}
同樣添加accept方法
#[inline]
pub fn accept<IO>(&self, stream: IO) -> io::Result<Accept<IO>>
where
IO: AsyncRead + AsyncWrite + Unpin,
{
self.accept_with(stream, |_| ())
}
pub fn accept_with<IO, F>(&self, stream: IO, f: F) -> io::Result<Accept<IO>>
where
IO: AsyncRead + AsyncWrite + Unpin,
F: FnOnce(&mut ServerConnection),
{
if let Some(a) = &self.accepter {
Ok(a.accept_with(stream, f))
} else {
self.check_and_request_cert()
.map_err(|_| io::Error::new(io::ErrorKind::Other, "load https error"))?;
Err(io::Error::new(io::ErrorKind::Other, "try next https error"))
}
}
當accepter
未初始化時,我們將會試圖檢查證書,查看是否能簽發證書。
此處我們為了避免併發中,重覆多次請求導致請求數過多導致的服務不可用,我們此處定義了全局靜態變數。
lazy_static! {
static ref CACHE_REQUEST: Mutex<HashMap<String, Instant>> = Mutex::new(HashMap::new());
}
在檢查的時候,我們只允許一段時間內僅有一個請求進入申請證書的流程,其它的請求全部返回錯誤:
let mut map = CACHE_REQUEST
.lock()
.map_err(|_| io::Error::new(io::ErrorKind::Other, "Fail get Lock"))?;
if let Some(last) = map.get(self.domain.as_ref().unwrap()) {
if last.elapsed() < Duration::from_secs(30) {
return Err(io::Error::new(io::ErrorKind::Other, "等待上次請求結束").into());
}
}
map.insert(self.domain.clone().unwrap(), Instant::now());
然後我們對該功能變數名稱發起證書簽名請求,此處我們會迴圈卡住整個線程,而非非同步的請求,所以我們這裡用了thread::spawn
而非tokio::spawn
:
let obj = self.clone();
thread::spawn(move || {
let _ = obj.request_cert();
});
以下是請求證書的函數:
fn request_cert(&self) -> Result<(), Error> {
// 使用let's encrypt簽發證書
let url = DirectoryUrl::LetsEncrypt;
let path = Path::new(".well-known/acme-challenge");
if !path.exists() {
let _ = std::fs::create_dir_all(path);
}
// 使用記憶體的存儲結構,存儲自己做處理
let persist = MemoryPersist::new();
// 創建目錄節點
let dir = Directory::from_url(persist, url)?;
// 設置請求的email信息
let acc = dir.account("[email protected]")?;
// 請求簽發的功能變數名稱
let mut ord_new = acc.new_order(&self.domain.clone().unwrap_or_default(), &[])?;
let start = Instant::now();
// 以下功能變數名稱的鑒權,需要等待let's encrypt確認信息
let ord_csr = loop {
// 成功簽發,跳出迴圈
if let Some(ord_csr) = ord_new.confirm_validations() {
break ord_csr;
}
// 超時30秒,認為失敗了
if start.elapsed() > Duration::from_secs(30) {
println!("獲取證書超時");
return Ok(());
}
// 獲取鑒權方式
let auths = ord_new.authorizations()?;
// 以下是HTTP的請求方法,本質上是請求token的url,然後返回正確的值
// 此處我們用的是臨時伺服器
//
// /var/www/.well-known/acme-challenge/<token>
//
// http://mydomain.io/.well-known/acme-challenge/<token>
let chall = auths[0].http_challenge();
// 將token存儲在目錄下
let token = chall.http_token();
let path = format!(".well-known/acme-challenge/{}", token);
// 獲取token的內容
let proof = chall.http_proof();
Helper::write_to_file(&path, proof.as_bytes())?;
// 等待acme檢測時間,以ms計
chall.validate(5000)?;
// 再嘗試刷新acme請求
ord_new.refresh()?;
};
// 創建rsa的密鑰對
let pkey_pri = create_rsa_key(2048);
// 提交CSR獲取最終的簽名
let ord_cert = ord_csr.finalize_pkey(pkey_pri, 5000)?;
// 下載簽名及證書,此時下載下來的為pkcs#8證書格式
let cert = ord_cert.download_and_save_cert()?;
Helper::write_to_file(
&self.get_cert_path().unwrap(),
cert.certificate().as_bytes(),
)?;
Helper::write_to_file(&self.get_key_path().unwrap(), cert.private_key().as_bytes())?;
Ok(())
}
在其中,我們跟acme伺服器的時候我們需要架設臨時文件伺服器以使acme訪問我們http伺服器的時候http://mydomain.io/.well-known/acme-challenge/<token>
能正確的返回正常的請求,我們將在綁定tls的時候,如果沒有該證書信息時,我們將自動添加一個.well-known/acme-challenge
的location以啟用https的驗證:
pub async fn bind(
&mut self,
) -> ProxyResult<(Vec<Option<WrapTlsAccepter>>, Vec<bool>, Vec<TcpListener>)> {
// ...
for value in &mut self.server {
// ...
if has_acme {
let mut location = LocationConfig::new();
let file_server = FileServer::new(
".well-known/acme-challenge".to_string(),
"/.well-known/acme-challenge".to_string(),
);
location.rule = Matcher::from_str("/.well-known/acme-challenge/").expect("matcher error");
location.file_server = Some(file_server);
value.location.insert(0, location);
}
}
Ok((accepters, tlss, listeners))
}
以啟用遠程acme能訪問該鏈接的能力,也就意味著我們不能將敏感信息放置在".well-known/acme-challenge"
目錄下麵,也就是我們使用MemoryPersist
的原因。
測試是否可行
因為http-01的方式必須使acme能訪問我們的伺服器,所以此時測試需要公網環境下進行測試:
我們配置如下文件,reverse.toml:
# 反向代理相關,七層協議為http及https
[http]
# 反向代理中的具體服務,可配置多個多組
[[http.server]]
bind_addr = "0.0.0.0:80"
bind_ssl = "0.0.0.0:443"
up_name = "auto1.wmproxy.net"
root = ""
[[http.server.location]]
rule = "/"
static_response = "I'm Ok {client_ip}"
此時佈置在我們的auto1.wmproxy.net
的伺服器上,我們運行
wmproxy run -c reverse.toml
此時當我們訪問https://auto1.wmproxy.net
的請求的時候,將會觸發證書申請,成功後證書將放置在".well-known"
下麵,下次啟動伺服器的時候我們將自動載入已請求的tls證書以提供https服務。
頻繁限制問題
在let's encrypt中,如果有早過5次成功後,需要2天後才能繼續申請,他將無限返回429,得註意控制申請證書的頻率。
總結
TLS證書在當今互聯網中處於最重要的一環,他保護著我們的隱私數據的安全,也是最流行的加密方式之一。所以TLS證書的快速部署對於小而美的應用能讓其快速的落地使用。
點擊 [關註],[在看],[點贊] 是對作者最大的支持