init
This commit is contained in:
commit
5c5a9b1c9a
6 changed files with 3391 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
/target
|
||||||
|
config.toml
|
3086
Cargo.lock
generated
Normal file
3086
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
25
Cargo.toml
Normal file
25
Cargo.toml
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
[package]
|
||||||
|
name = "proxmox-mtls-gateway"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
async-trait="0.1"
|
||||||
|
pingora-proxy = { path = "../pingora/pingora-proxy" }
|
||||||
|
pingora-core = { path = "../pingora/pingora-core" }
|
||||||
|
pingora-http = { path = "../pingora/pingora-http" }
|
||||||
|
log = "0.4"
|
||||||
|
clap = "3.2"
|
||||||
|
env_logger = "0.11"
|
||||||
|
urlencoding = "2.1.3"
|
||||||
|
bytes = "1.6.0"
|
||||||
|
|
||||||
|
reqwest = { version = "0.12", features = [ "json" ] }
|
||||||
|
url = "2.5.2"
|
||||||
|
serde = "1.0.210"
|
||||||
|
serde_urlencoded = "0.7.1"
|
||||||
|
toml = "0.8.19"
|
||||||
|
http = "1.1.0"
|
||||||
|
serde_json = "1.0.128"
|
4
README.md
Normal file
4
README.md
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
[Proxmox VE](https://www.proxmox.com/en/proxmox-ve) currently doesn't support authentication with TLS client certificates.
|
||||||
|
This project implements a reverse proxy that authenticates a user using TLS client certificates.
|
||||||
|
After successful authentication the proxy creates a new session ticket using the PVE API using the mapped credentials in `config.toml` and injects it into the session.
|
||||||
|
|
15
config.example.toml
Normal file
15
config.example.toml
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
listen_addr = "[::]:8080"
|
||||||
|
|
||||||
|
[tls]
|
||||||
|
ca_file = "/foo/ca.pem"
|
||||||
|
cert_file = "/foo/server.crt"
|
||||||
|
key_file = "/foo/server.key"
|
||||||
|
|
||||||
|
[proxmox]
|
||||||
|
host = "127.0.0.1"
|
||||||
|
port = 8006
|
||||||
|
|
||||||
|
[users."john@example.com"]
|
||||||
|
username = "john"
|
||||||
|
password = "verysecureandlongpassword"
|
||||||
|
realm = "pve"
|
259
src/main.rs
Normal file
259
src/main.rs
Normal file
|
@ -0,0 +1,259 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use clap::Parser;
|
||||||
|
use log::{debug, info, warn};
|
||||||
|
use pingora_core::server::configuration::Opt;
|
||||||
|
use pingora_core::Result;
|
||||||
|
use pingora_core::{protocols::ALPN, server::Server, upstreams::peer::Peer};
|
||||||
|
use pingora_core::{tls::ssl::SslVerifyMode, upstreams::peer::HttpPeer};
|
||||||
|
use pingora_http::{RequestHeader, ResponseHeader};
|
||||||
|
use pingora_proxy::{ProxyHttp, Session};
|
||||||
|
use reqwest::{Client, Url};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Configuration file
|
||||||
|
*/
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct Config {
|
||||||
|
listen_addr: String,
|
||||||
|
tls: TlsConfig,
|
||||||
|
proxmox: ProxmoxConfig,
|
||||||
|
users: HashMap<String, UserConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct TlsConfig {
|
||||||
|
ca_file: String,
|
||||||
|
cert_file: String,
|
||||||
|
key_file: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct ProxmoxConfig {
|
||||||
|
host: String,
|
||||||
|
port: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
pub struct UserConfig {
|
||||||
|
username: String,
|
||||||
|
password: String,
|
||||||
|
realm: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct ProxmoxLoginReq<'a> {
|
||||||
|
username: &'a str,
|
||||||
|
password: &'a str,
|
||||||
|
realm: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ProxmoxVE API
|
||||||
|
*/
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
struct ProxmoxRes<T> {
|
||||||
|
data: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
|
pub struct ProxmoxSession {
|
||||||
|
pub username: String,
|
||||||
|
#[serde(rename = "CSRFPreventionToken")]
|
||||||
|
pub csrf_token: String,
|
||||||
|
pub ticket: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MTLSGateway {
|
||||||
|
config: Config,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Context {
|
||||||
|
subject_name: String,
|
||||||
|
user: UserConfig,
|
||||||
|
session: Option<ProxmoxSession>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl ProxyHttp for MTLSGateway {
|
||||||
|
type CTX = Context;
|
||||||
|
fn new_ctx(&self) -> Self::CTX {
|
||||||
|
Context {
|
||||||
|
subject_name: "".to_string(),
|
||||||
|
user: UserConfig {
|
||||||
|
username: "".to_string(),
|
||||||
|
password: "".to_string(),
|
||||||
|
realm: "".to_string(),
|
||||||
|
},
|
||||||
|
session: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn request_filter(&self, session: &mut Session, ctx: &mut Self::CTX) -> Result<bool> {
|
||||||
|
if let Some(cert) = &session
|
||||||
|
.downstream_session
|
||||||
|
.digest()
|
||||||
|
.and_then(|x| x.ssl_digest.as_ref())
|
||||||
|
.map(|x| &x.certificate)
|
||||||
|
{
|
||||||
|
let subjects = cert
|
||||||
|
.subject_name()
|
||||||
|
.entries()
|
||||||
|
.filter_map(|x| x.data().as_utf8().ok())
|
||||||
|
.map(|x| x.to_string());
|
||||||
|
if let Some((subject_name, user_config)) = subjects
|
||||||
|
.filter_map(|x| self.config.users.get(&x).map(|y| (x, y)))
|
||||||
|
.next()
|
||||||
|
{
|
||||||
|
ctx.user = user_config.clone();
|
||||||
|
ctx.subject_name = subject_name;
|
||||||
|
Ok(false)
|
||||||
|
} else {
|
||||||
|
session.respond_error(403 /* FORBIDDEN */).await?;
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
session.respond_error(401 /* UNAUTHORIZED */).await?;
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn upstream_peer(
|
||||||
|
&self,
|
||||||
|
_session: &mut Session,
|
||||||
|
_ctx: &mut Self::CTX,
|
||||||
|
) -> Result<Box<HttpPeer>> {
|
||||||
|
let mut peer = HttpPeer::new(
|
||||||
|
(self.config.proxmox.host.as_str(), self.config.proxmox.port),
|
||||||
|
true,
|
||||||
|
self.config.proxmox.host.clone(),
|
||||||
|
);
|
||||||
|
|
||||||
|
peer.get_mut_peer_options().unwrap().alpn = ALPN::H2;
|
||||||
|
|
||||||
|
Ok(Box::new(peer))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn upstream_request_filter(
|
||||||
|
&self,
|
||||||
|
_session: &mut Session,
|
||||||
|
upstream_request: &mut RequestHeader,
|
||||||
|
ctx: &mut Self::CTX,
|
||||||
|
) -> Result<()> {
|
||||||
|
if !upstream_request.headers.contains_key("Cookie") {
|
||||||
|
info!(
|
||||||
|
"Bootstrapping session for {} using {}@{}",
|
||||||
|
ctx.subject_name, ctx.user.username, ctx.user.realm
|
||||||
|
);
|
||||||
|
let session = login(&ctx.user, &self.config).await.unwrap();
|
||||||
|
upstream_request.insert_header(
|
||||||
|
"Cookie",
|
||||||
|
format!("PVEAuthCookie={}", urlencoding::encode(&session.ticket)),
|
||||||
|
)?;
|
||||||
|
ctx.session = Some(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn response_filter(
|
||||||
|
&self,
|
||||||
|
_session: &mut Session,
|
||||||
|
upstream_response: &mut ResponseHeader,
|
||||||
|
ctx: &mut Self::CTX,
|
||||||
|
) -> Result<()>
|
||||||
|
where
|
||||||
|
Self::CTX: Send + Sync,
|
||||||
|
{
|
||||||
|
// replace existing header if any
|
||||||
|
upstream_response
|
||||||
|
.insert_header("Server", "PVE mTLS Gateway")
|
||||||
|
.unwrap();
|
||||||
|
if let Some(session) = &ctx.session {
|
||||||
|
upstream_response.append_header(
|
||||||
|
"Set-Cookie",
|
||||||
|
format!(
|
||||||
|
"PVEAuthCookie={};Path=/;SameSite=Strict;Secure",
|
||||||
|
urlencoding::encode(&session.ticket)
|
||||||
|
),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// because we don't support h3
|
||||||
|
upstream_response.remove_header("alt-svc");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn logging(
|
||||||
|
&self,
|
||||||
|
session: &mut Session,
|
||||||
|
_e: Option<&pingora_core::Error>,
|
||||||
|
ctx: &mut Self::CTX,
|
||||||
|
) {
|
||||||
|
let response_code = session
|
||||||
|
.response_written()
|
||||||
|
.map_or(0, |resp| resp.status.as_u16());
|
||||||
|
debug!(
|
||||||
|
"{} response code: {response_code}",
|
||||||
|
self.request_summary(session, ctx)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
env_logger::init();
|
||||||
|
|
||||||
|
let config = std::fs::read_to_string(std::env::var("CONFIG").expect("config file path"))
|
||||||
|
.expect("user mapping file");
|
||||||
|
let config: Config = toml::from_str(&config).expect("valid user mapping");
|
||||||
|
let listen_addr = config.listen_addr.clone();
|
||||||
|
|
||||||
|
// read command line arguments
|
||||||
|
let opt = Opt::parse();
|
||||||
|
let mut my_server = Server::new(Some(opt)).unwrap();
|
||||||
|
my_server.bootstrap();
|
||||||
|
|
||||||
|
let mut tls_settings = pingora_core::listeners::TlsSettings::intermediate(
|
||||||
|
&config.tls.cert_file,
|
||||||
|
&config.tls.key_file,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
tls_settings.enable_h2();
|
||||||
|
tls_settings.set_verify(SslVerifyMode::all());
|
||||||
|
tls_settings.set_ca_file(&config.tls.ca_file).unwrap();
|
||||||
|
|
||||||
|
let mut my_proxy =
|
||||||
|
pingora_proxy::http_proxy_service(&my_server.configuration, MTLSGateway { config });
|
||||||
|
my_proxy.add_tls_with_settings(&listen_addr, None, tls_settings);
|
||||||
|
|
||||||
|
my_server.add_service(my_proxy);
|
||||||
|
my_server.run_forever()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn proxmox_url(path: &str, config: &Config) -> Result<Url, url::ParseError> {
|
||||||
|
Url::parse(&format!(
|
||||||
|
"https://{}:{}{}",
|
||||||
|
config.proxmox.host, config.proxmox.port, path
|
||||||
|
))
|
||||||
|
}
|
||||||
|
async fn login(user: &UserConfig, config: &Config) -> Result<ProxmoxSession, reqwest::Error> {
|
||||||
|
let client = Client::new();
|
||||||
|
let res = client
|
||||||
|
.post(proxmox_url("/api2/json/access/ticket", config).unwrap())
|
||||||
|
.json(&ProxmoxLoginReq {
|
||||||
|
username: &user.username,
|
||||||
|
password: &user.password,
|
||||||
|
realm: &user.realm,
|
||||||
|
})
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.error_for_status()?;
|
||||||
|
|
||||||
|
let sess = res.json::<ProxmoxRes<ProxmoxSession>>().await?.data;
|
||||||
|
Ok(sess)
|
||||||
|
}
|
Loading…
Reference in a new issue