init
Signed-off-by: eternal-flame-AD <yume@yumechi.jp>
This commit is contained in:
commit
54b1bc412a
7 changed files with 1891 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/target
|
1560
Cargo.lock
generated
Normal file
1560
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
19
Cargo.toml
Normal file
19
Cargo.toml
Normal file
|
@ -0,0 +1,19 @@
|
|||
[package]
|
||||
name = "pam-apppasswd-hashicorp-vault"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
build = "build.rs"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
reqwest = { version = "0.12.9", default-features = false, features = ["charset", "json", "blocking", "rustls-tls", "rustls-tls-native-roots"] }
|
||||
serde = { version = "1.0.214", features = ["derive"] }
|
||||
|
||||
[build-dependencies]
|
||||
bindgen = "0.70.1"
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
|
31
build.rs
Normal file
31
build.rs
Normal file
|
@ -0,0 +1,31 @@
|
|||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[allow(deprecated, reason = "False positive")]
|
||||
use bindgen::CargoCallbacks;
|
||||
|
||||
fn main() {
|
||||
println!("cargo:rustc-link-lib=pam");
|
||||
|
||||
// The bindgen::Builder is the main entry point
|
||||
// to bindgen, and lets you build up options for
|
||||
// the resulting bindings.
|
||||
let bindings = bindgen::Builder::default()
|
||||
// The input header we would like to generate
|
||||
// bindings for.
|
||||
.header("wrapper.h")
|
||||
.default_macro_constant_type(bindgen::MacroTypeVariation::Signed)
|
||||
// Tell cargo to invalidate the built crate whenever any of the
|
||||
// included header files changed.
|
||||
.parse_callbacks(Box::new(CargoCallbacks::new()))
|
||||
// Finish the builder and generate the bindings.
|
||||
.generate()
|
||||
// Unwrap the Result and panic on failure.
|
||||
.expect("Unable to generate bindings");
|
||||
|
||||
// Write the bindings to the $OUT_DIR/bindings.rs file.
|
||||
let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
|
||||
bindings
|
||||
.write_to_file(out_path.join("pam_sys.rs"))
|
||||
.expect("Couldn't write bindings!");
|
||||
}
|
273
src/lib.rs
Normal file
273
src/lib.rs
Normal file
|
@ -0,0 +1,273 @@
|
|||
#![warn(clippy::pedantic)]
|
||||
#![allow(clippy::missing_errors_doc)]
|
||||
|
||||
use std::ffi::{c_char, c_int, CStr};
|
||||
use std::io::Write;
|
||||
|
||||
use pam_sys::{pam_get_authtok, pam_get_user, pam_handle_t, PAM_ABORT, PAM_AUTH_ERR, PAM_SUCCESS};
|
||||
use serde::Deserialize;
|
||||
|
||||
pub mod pam_sys;
|
||||
|
||||
macro_rules! check_result {
|
||||
($e:expr) => {{
|
||||
let ret = $e;
|
||||
match ret {
|
||||
Ok(r) => r,
|
||||
Err(ret) => {
|
||||
eprintln!("Error: {:?}", ret);
|
||||
return PAM_ABORT;
|
||||
}
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
pub struct Environment {
|
||||
vault_url: String,
|
||||
vault_token: String,
|
||||
mount_point: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct VaultResponse<D> {
|
||||
data: D,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct KVData {
|
||||
data: Option<KVDataData>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct KVDataData {
|
||||
password: Option<String>,
|
||||
}
|
||||
|
||||
pub fn renew_token(env: &Environment) -> Result<bool, reqwest::Error> {
|
||||
let client = reqwest::blocking::Client::new();
|
||||
|
||||
let url = format!("{}/v1/auth/token/renew-self", env.vault_url);
|
||||
|
||||
let res = client
|
||||
.post(&url)
|
||||
.header("X-Vault-Token", env.vault_token.as_str())
|
||||
.header("Content-Type", "application/json")
|
||||
.body(r#"{"increment": "3d"}"#)
|
||||
.send()?
|
||||
.error_for_status()?;
|
||||
|
||||
Ok(res.status().is_success())
|
||||
}
|
||||
|
||||
impl Environment {
|
||||
pub fn from_env() -> Result<Self, std::env::VarError> {
|
||||
let vault_url = std::env::var("VAULT_ADDR")?;
|
||||
let vault_token = std::env::var("VAULT_TOKEN")?;
|
||||
let mount_point = std::env::var("VAULT_MOUNT_POINT")?;
|
||||
|
||||
Ok(Self {
|
||||
vault_url,
|
||||
vault_token,
|
||||
mount_point,
|
||||
})
|
||||
}
|
||||
pub fn from_env_file(file: &str) -> Result<Self, std::io::Error> {
|
||||
let f = std::fs::read_to_string(file)?;
|
||||
|
||||
let mut vault_url = String::new();
|
||||
let mut vault_token = String::new();
|
||||
let mut mount_point = String::new();
|
||||
|
||||
for line in f.lines().filter(|l| !l.is_empty() && !l.starts_with('#')) {
|
||||
let (key, value) = line.split_once('=').unwrap_or((line, ""));
|
||||
|
||||
match key {
|
||||
"VAULT_ADDR" => vault_url = value.trim().to_string(),
|
||||
"VAULT_TOKEN" => vault_token = value.trim().to_string(),
|
||||
"VAULT_MOUNT_POINT" => mount_point = value.trim().to_string(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
vault_url,
|
||||
vault_token,
|
||||
mount_point,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! maybe_write {
|
||||
($f:expr, $fmt:expr $(, $arg:tt)*) => {
|
||||
if let Some(f) = $f.as_mut() {
|
||||
writeln!(f, $fmt $(,$arg)*).expect("Could not write to debug file");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn check_vault(
|
||||
log: &mut Option<impl Write>,
|
||||
env: &Environment,
|
||||
user: &str,
|
||||
key: &str,
|
||||
token: &str,
|
||||
) -> Result<bool, reqwest::Error> {
|
||||
let client = reqwest::blocking::Client::new();
|
||||
|
||||
let url = format!(
|
||||
"{}/v1/{}/data/{}/{}",
|
||||
env.vault_url,
|
||||
env.mount_point,
|
||||
path_encode(user),
|
||||
key
|
||||
);
|
||||
|
||||
maybe_write!(log, "url: {}", url);
|
||||
|
||||
if let Err(e) = renew_token(env) {
|
||||
maybe_write!(log, "Error renewing token: {:?}", e);
|
||||
}
|
||||
|
||||
let res = client
|
||||
.get(&url)
|
||||
.header("X-Vault-Token", env.vault_token.as_str())
|
||||
.send()?
|
||||
.error_for_status()?
|
||||
.json::<VaultResponse<KVData>>()?;
|
||||
|
||||
if let Some(data) = res.data.data {
|
||||
if let Some(password) = data.password {
|
||||
return Ok(password == token);
|
||||
}
|
||||
|
||||
maybe_write!(log, "Password not found in response");
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
fn path_encode(path: &str) -> String {
|
||||
path.chars()
|
||||
.map(|c| match c {
|
||||
'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' => c.to_string(),
|
||||
_ => format!("%{:02X}", c as u8),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn valid_user(user: &str) -> bool {
|
||||
user.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || "_-.@".contains(c))
|
||||
}
|
||||
|
||||
fn valid_token(token: &str) -> bool {
|
||||
token
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || "!@#$%^&*()_+-=".contains(c))
|
||||
}
|
||||
|
||||
fn valid_token_key(token_key: &str) -> bool {
|
||||
token_key
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || "_".contains(c))
|
||||
}
|
||||
|
||||
/// This function is called by PAM to authenticate the user.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// This function must be called by PAM with the correct arguments defined by the PAM API.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn pam_sm_authenticate(
|
||||
pamh: *mut pam_handle_t,
|
||||
_flags: c_int,
|
||||
arg_count: c_int,
|
||||
arg_value: *const *const c_char,
|
||||
) -> c_int {
|
||||
let mut vault_conf = "/etc/vault.conf";
|
||||
let mut debug = false;
|
||||
|
||||
for i in 0..arg_count {
|
||||
let arg = *arg_value.offset(i as isize);
|
||||
let arg = check_result!(std::ffi::CStr::from_ptr(arg).to_str());
|
||||
if arg == "debug" {
|
||||
debug = true;
|
||||
}
|
||||
|
||||
if arg.starts_with("vault_conf=") {
|
||||
vault_conf = arg.split_once('=').unwrap().1;
|
||||
}
|
||||
}
|
||||
|
||||
let mut debug_out = if debug {
|
||||
Some(
|
||||
std::fs::OpenOptions::new()
|
||||
.append(true)
|
||||
.create(true)
|
||||
.open("/tmp/pam_vault_debug.log")
|
||||
.expect("Could not open debug file"),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let env = check_result!(Environment::from_env_file(vault_conf));
|
||||
|
||||
let mut user = std::ptr::null();
|
||||
|
||||
if PAM_SUCCESS != pam_get_user(pamh, &mut user, std::ptr::null()) {
|
||||
return PAM_AUTH_ERR;
|
||||
};
|
||||
|
||||
let mut token = std::ptr::null();
|
||||
if PAM_SUCCESS != pam_get_authtok(pamh, pam_sys::PAM_AUTHTOK, &mut token, std::ptr::null()) {
|
||||
return PAM_AUTH_ERR;
|
||||
};
|
||||
|
||||
let (user, token) = (
|
||||
check_result!(CStr::from_ptr(user as *const c_char).to_str()),
|
||||
check_result!(CStr::from_ptr(token as *const c_char).to_str()),
|
||||
);
|
||||
|
||||
let (token_key, token_value) = token.split_once('=').unwrap_or_else(|| ("default", &token));
|
||||
|
||||
if user.is_empty()
|
||||
|| token_value.len() < 8
|
||||
|| !valid_token_key(token_key)
|
||||
|| !valid_user(&user)
|
||||
|| !valid_token(&token)
|
||||
{
|
||||
return PAM_AUTH_ERR;
|
||||
}
|
||||
|
||||
maybe_write!(debug_out, "token_key: {}", token_key);
|
||||
|
||||
match check_vault(&mut debug_out, &env, &user, token_key, token_value) {
|
||||
Ok(true) => PAM_SUCCESS,
|
||||
Ok(false) => PAM_AUTH_ERR,
|
||||
Err(e) => {
|
||||
eprintln!("Error: {e:?}");
|
||||
PAM_ABORT
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn pam_sm_acct_mgmt(
|
||||
_pamh: *mut pam_handle_t,
|
||||
_flags: c_int,
|
||||
_arg_count: c_int,
|
||||
_arg_value: *const *const c_char,
|
||||
) -> c_int {
|
||||
PAM_SUCCESS
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn pam_sm_setcred(
|
||||
_pamh: *mut pam_handle_t,
|
||||
_flags: c_int,
|
||||
_arg_count: c_int,
|
||||
_arg_value: *const *const c_char,
|
||||
) -> c_int {
|
||||
PAM_SUCCESS
|
||||
}
|
5
src/pam_sys.rs
Normal file
5
src/pam_sys.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
#![allow(clippy::all)]
|
||||
#![allow(clippy::pedantic)]
|
||||
#![allow(non_camel_case_types)]
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/pam_sys.rs"));
|
2
wrapper.h
Normal file
2
wrapper.h
Normal file
|
@ -0,0 +1,2 @@
|
|||
#include <security/pam_modules.h>
|
||||
#include <security/pam_ext.h>
|
Loading…
Reference in a new issue