Add tls generator to main kanidmd (#1743)

This commit is contained in:
Firstyear 2023-06-19 20:51:44 +10:00 committed by GitHub
parent 32a7200305
commit 8b331325ae
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 503 additions and 28 deletions

View file

@ -60,6 +60,7 @@ build/kanidmd: ## Build the kanidmd docker image locally
build/kanidmd:
@$(CONTAINER_TOOL) build $(CONTAINER_TOOL_ARGS) -f server/Dockerfile \
-t $(IMAGE_BASE)/server:$(IMAGE_VERSION) \
--platform $(IMAGE_ARCH) \
--build-arg "KANIDM_BUILD_PROFILE=container_generic" \
--build-arg "KANIDM_FEATURES=" \
$(CONTAINER_BUILD_ARGS) .
@ -68,6 +69,7 @@ build/kanidmd:
build/radiusd: ## Build the radiusd docker image locally
build/radiusd:
@$(CONTAINER_TOOL) build $(CONTAINER_TOOL_ARGS) \
--platform $(IMAGE_ARCH) \
-f rlm_python/Dockerfile \
-t $(IMAGE_BASE)/radius:$(IMAGE_VERSION) .

View file

@ -1,6 +1,7 @@
# Kanidm
- [Introduction to Kanidm](intro.md)
- [Evaluation Quickstart](quickstart.md)
- [Installing the Server](installing_the_server.md)
- [Choosing a Domain Name](choosing_a_domain_name.md)
- [Preparing for your Deployment](prepare_the_server.md)

83
book/src/quickstart.md Normal file
View file

@ -0,0 +1,83 @@
# Evaluation Quickstart
This section will guide you through a quick setup of Kanidm for evaluation. It's recommended that
for a production deployment you follow the steps in the
[installation chapter](installing_the_server.html) instead as there are a number of security
considerations you should understand.
### Requirements
- docker or podman
- `x86_64` cpu supporting `x86_64_v2` OR `aarch64` cpu supporting `neon`
### Get the software
```bash
docker pull kanidm/server:latest
```
### Configure the container
```bash
docker volume create kanidmd
docker create --name kanidmd \
-p 443:8443 \
-p 636:3636 \
-v kanidmd:/data \
kanidm/server:latest
```
### Configure the server
Create server.toml
```toml
{{#rustdoc_include ../../examples/server_container.toml}}
```
### Add configuration to container
```bash
docker cp server.toml kanidmd:/data/server.toml
```
### Generate evaluation certificates
```bash
docker run --rm -i -t -v kanidmd:/data \
kanidm/server:latest \
kanidmd cert-generate -c /data/server.toml
```
### Recover the admin password
```bash
docker run --rm -i -t -v kanidmd:/data \
kanidm/server:latest \
kanidmd recover-account admin -c /data/server.toml
```
### Start Kanidmd
```bash
docker start kanidmd
```
### Setup the client configuration
```toml
# ~/.config/kanidm
uri = "https://localhost:443"
verify_ca = false
```
### Check you can login
```bash
kanidm login
```
### What next?
You can now follow the steps in the [administration section](administrivia.md)

View file

@ -19,8 +19,8 @@ auth_token = "putyourtokenhere"
radius_cert_path = "/certs/cert.pem" # the TLS certificate
radius_key_path = "/certs/key.pem" # the signing key for radius TLS
radius_dh_path = "/certs/dh.pem" # the diffie-hellman output
radius_ca_path = "/certs/ca.pem" # the CA certificate
radius_dh_path = "/certs/dh.pem" # the diffie-hellman output
# A list of groups, if a user is in them, they're approved for RADIUS authentication
radius_required_groups = [

View file

@ -47,7 +47,6 @@ RUN python3 -m pip install --no-cache-dir --no-warn-script-location /pkg/pykanid
COPY rlm_python/radius_entrypoint.py /radius_entrypoint.py
ENV LD_PRELOAD=/usr/lib64/libpython3.so
ENV KANIDM_CONFIG_FILE="/data/kanidm"
RUN chmod a+r /etc/raddb/certs/ -R

View file

@ -79,12 +79,15 @@ def setup_certs(
if kanidm_config_object.radius_dh_path is not None:
cert_dh = Path(kanidm_config_object.radius_dh_path).expanduser().resolve()
if not cert_dh.exists():
print(f"Failed to find radiusd dh file ({cert_dh}), quitting!", file=sys.stderr)
sys.exit(1)
# print(f"Failed to find radiusd dh file ({cert_dh}), quitting!", file=sys.stderr)
# sys.exit(1)
print(f"Generating dh params in {cert_dh}")
subprocess.check_call(["openssl", "dhparam", "-out", cert_dh, "2048"])
if cert_dh != CERT_DH_DEST:
print(f"Copying {cert_dh} to {CERT_DH_DEST}")
shutil.copyfile(cert_dh, CERT_DH_DEST)
server_key = Path(kanidm_config_object.radius_key_path).expanduser().resolve()
if not server_key.exists() or not server_key.is_file():
print(

View file

@ -1,23 +1,29 @@
#!/bin/bash
set -x
if [ -z "${IMAGE}" ]; then
IMAGE="kanidm/radius:devel"
fi
echo "Running docker container: ${IMAGE}"
if [ ! -z "${IMAGE_ARCH}" ]; then
IMAGE_ARCH="--platform ${IMAGE_ARCH}"
fi
if [ -z "${CONFIG_FILE}" ]; then
CONFIG_FILE="$(pwd)/../examples/kanidm"
fi
echo "Using config file: ${CONFIG_FILE}"
if [ ! -d "/tmp/kanidm/" ]; then
echo "Can't find /tmp/kanidm - you might need to run insecure_generate_tls.sh"
echo "Can't find /tmp/kanidm - you may need to run run_insecure_dev_server"
fi
echo "Starting the dev container..."
#shellcheck disable=SC2068
docker run --rm -it \
${IMAGE_ARCH} \
--network host \
--name radiusd \
-v /tmp/kanidm/:/data/ \

View file

@ -1,11 +1,28 @@
//! This module contains cryptographic setup code, a long with what policy
//! and ciphers we accept.
use openssl::ec::{EcGroup, EcKey};
use openssl::error::ErrorStack;
use openssl::nid::Nid;
use openssl::ssl::{SslAcceptor, SslAcceptorBuilder, SslFiletype, SslMethod};
use openssl::x509::{
extension::{
AuthorityKeyIdentifier, BasicConstraints, KeyUsage, SubjectAlternativeName,
SubjectKeyIdentifier,
},
X509NameBuilder, X509ReqBuilder, X509,
};
use openssl::{asn1, bn, hash, pkey};
use crate::config::Configuration;
use std::fs::File;
use std::io::{Read, Write};
use std::path::Path;
const CA_VALID_DAYS: u32 = 30;
const CERT_VALID_DAYS: u32 = 5;
/// From the server configuration, generate an OpenSSL acceptor that we can use
/// to build our sockets for https/ldaps.
pub fn setup_tls(config: &Configuration) -> Result<Option<SslAcceptorBuilder>, ErrorStack> {
@ -20,3 +37,264 @@ pub fn setup_tls(config: &Configuration) -> Result<Option<SslAcceptorBuilder>, E
None => Ok(None),
}
}
fn get_group() -> Result<EcGroup, ErrorStack> {
EcGroup::from_curve_name(Nid::X9_62_PRIME256V1)
}
pub(crate) struct CaHandle {
key: pkey::PKey<pkey::Private>,
cert: X509,
}
pub(crate) fn write_ca(
key_ar: impl AsRef<Path>,
cert_ar: impl AsRef<Path>,
handle: &CaHandle,
) -> Result<(), ()> {
let key_path: &Path = key_ar.as_ref();
let cert_path: &Path = cert_ar.as_ref();
let key_pem = handle.key.private_key_to_pem_pkcs8().map_err(|e| {
error!(err = ?e, "Failed to convert key to PEM");
})?;
let cert_pem = handle.cert.to_pem().map_err(|e| {
error!(err = ?e, "Failed to convert cert to PEM");
})?;
File::create(key_path)
.and_then(|mut file| file.write_all(&key_pem))
.map_err(|e| {
error!(err = ?e, "Failed to create {:?}", key_path);
})?;
File::create(cert_path)
.and_then(|mut file| file.write_all(&cert_pem))
.map_err(|e| {
error!(err = ?e, "Failed to create {:?}", cert_path);
})
}
pub(crate) fn build_ca() -> Result<CaHandle, ErrorStack> {
let ecgroup = get_group()?;
let eckey = EcKey::generate(&ecgroup)?;
let ca_key = pkey::PKey::from_ec_key(eckey)?;
let mut x509_name = X509NameBuilder::new()?;
x509_name.append_entry_by_text("C", "AU")?;
x509_name.append_entry_by_text("ST", "QLD")?;
x509_name.append_entry_by_text("O", "Kanidm")?;
x509_name.append_entry_by_text("CN", "Kanidm Generated CA")?;
x509_name.append_entry_by_text("OU", "Development and Evaluation - NOT FOR PRODUCTION")?;
let x509_name = x509_name.build();
let mut cert_builder = X509::builder()?;
// Yes, 2 actually means 3 here ...
cert_builder.set_version(2)?;
let serial_number = bn::BigNum::from_u32(1).and_then(|serial| serial.to_asn1_integer())?;
cert_builder.set_serial_number(&serial_number)?;
cert_builder.set_subject_name(&x509_name)?;
cert_builder.set_issuer_name(&x509_name)?;
let not_before = asn1::Asn1Time::days_from_now(0)?;
cert_builder.set_not_before(&not_before)?;
let not_after = asn1::Asn1Time::days_from_now(CA_VALID_DAYS)?;
cert_builder.set_not_after(&not_after)?;
cert_builder.append_extension(BasicConstraints::new().critical().ca().pathlen(0).build()?)?;
cert_builder.append_extension(
KeyUsage::new()
.critical()
.key_cert_sign()
.crl_sign()
.build()?,
)?;
let subject_key_identifier =
SubjectKeyIdentifier::new().build(&cert_builder.x509v3_context(None, None))?;
cert_builder.append_extension(subject_key_identifier)?;
cert_builder.set_pubkey(&ca_key)?;
cert_builder.sign(&ca_key, hash::MessageDigest::sha256())?;
let ca_cert = cert_builder.build();
Ok(CaHandle {
key: ca_key,
cert: ca_cert,
})
}
pub(crate) fn load_ca(
ca_key_ar: impl AsRef<Path>,
ca_cert_ar: impl AsRef<Path>,
) -> Result<CaHandle, ()> {
let ca_key_path: &Path = ca_key_ar.as_ref();
let ca_cert_path: &Path = ca_cert_ar.as_ref();
let mut ca_key_pem = vec![];
File::open(ca_key_path)
.and_then(|mut file| file.read_to_end(&mut ca_key_pem))
.map_err(|e| {
error!(err = ?e, "Failed to read {:?}", ca_key_path);
})?;
let mut ca_cert_pem = vec![];
File::open(ca_cert_path)
.and_then(|mut file| file.read_to_end(&mut ca_cert_pem))
.map_err(|e| {
error!(err = ?e, "Failed to read {:?}", ca_cert_path);
})?;
let ca_key = pkey::PKey::private_key_from_pem(&ca_key_pem).map_err(|e| {
error!(err = ?e, "Failed to convert PEM to key");
})?;
let ca_cert = X509::from_pem(&ca_cert_pem).map_err(|e| {
error!(err = ?e, "Failed to convert PEM to cert");
})?;
Ok(CaHandle {
key: ca_key,
cert: ca_cert,
})
}
pub(crate) struct CertHandle {
key: pkey::PKey<pkey::Private>,
cert: X509,
chain: Vec<X509>,
}
pub(crate) fn write_cert(
key_ar: impl AsRef<Path>,
chain_ar: impl AsRef<Path>,
cert_ar: impl AsRef<Path>,
handle: &CertHandle,
) -> Result<(), ()> {
let key_path: &Path = key_ar.as_ref();
let chain_path: &Path = chain_ar.as_ref();
let cert_path: &Path = cert_ar.as_ref();
let key_pem = handle.key.private_key_to_pem_pkcs8().map_err(|e| {
error!(err = ?e, "Failed to convert key to PEM");
})?;
let cert_pem = handle.cert.to_pem().map_err(|e| {
error!(err = ?e, "Failed to convert cert to PEM");
})?;
let mut chain_pem = cert_pem.clone();
// Build the chain PEM.
for ca_cert in &handle.chain {
match ca_cert.to_pem() {
Ok(c) => {
chain_pem.extend_from_slice(&c);
}
Err(e) => {
error!(err = ?e, "Failed to convert cert to PEM");
return Err(());
}
}
}
File::create(key_path)
.and_then(|mut file| file.write_all(&key_pem))
.map_err(|e| {
error!(err = ?e, "Failed to create {:?}", key_path);
})?;
File::create(chain_path)
.and_then(|mut file| file.write_all(&chain_pem))
.map_err(|e| {
error!(err = ?e, "Failed to create {:?}", chain_path);
})?;
File::create(cert_path)
.and_then(|mut file| file.write_all(&cert_pem))
.map_err(|e| {
error!(err = ?e, "Failed to create {:?}", cert_path);
})
}
pub(crate) fn build_cert(
domain_name: &str,
ca_handle: &CaHandle,
) -> Result<CertHandle, ErrorStack> {
let ecgroup = get_group()?;
let eckey = EcKey::generate(&ecgroup)?;
let int_key = pkey::PKey::from_ec_key(eckey)?;
//
let mut req_builder = X509ReqBuilder::new()?;
req_builder.set_pubkey(&int_key)?;
let mut x509_name = X509NameBuilder::new()?;
x509_name.append_entry_by_text("C", "AU")?;
x509_name.append_entry_by_text("ST", "QLD")?;
x509_name.append_entry_by_text("O", "Kanidm")?;
x509_name.append_entry_by_text("CN", domain_name)?;
// Requirement of packed attestation.
x509_name.append_entry_by_text("OU", "Development and Evaluation - NOT FOR PRODUCTION")?;
let x509_name = x509_name.build();
req_builder.set_subject_name(&x509_name)?;
req_builder.sign(&int_key, hash::MessageDigest::sha256())?;
let req = req_builder.build();
// ==
let mut cert_builder = X509::builder()?;
// Yes, 2 actually means 3 here ...
cert_builder.set_version(2)?;
let serial_number = bn::BigNum::from_u32(2).and_then(|serial| serial.to_asn1_integer())?;
cert_builder.set_pubkey(&int_key)?;
cert_builder.set_serial_number(&serial_number)?;
cert_builder.set_subject_name(req.subject_name())?;
cert_builder.set_issuer_name(ca_handle.cert.subject_name())?;
let not_before = asn1::Asn1Time::days_from_now(0)?;
cert_builder.set_not_before(&not_before)?;
let not_after = asn1::Asn1Time::days_from_now(CERT_VALID_DAYS)?;
cert_builder.set_not_after(&not_after)?;
cert_builder.append_extension(BasicConstraints::new().build()?)?;
cert_builder.append_extension(
KeyUsage::new()
.critical()
.non_repudiation()
.digital_signature()
.key_encipherment()
.build()?,
)?;
let subject_key_identifier = SubjectKeyIdentifier::new()
.build(&cert_builder.x509v3_context(Some(&ca_handle.cert), None))?;
cert_builder.append_extension(subject_key_identifier)?;
let auth_key_identifier = AuthorityKeyIdentifier::new()
.keyid(false)
.issuer(false)
.build(&cert_builder.x509v3_context(Some(&ca_handle.cert), None))?;
cert_builder.append_extension(auth_key_identifier)?;
let subject_alt_name = SubjectAlternativeName::new()
.dns(domain_name)
.build(&cert_builder.x509v3_context(Some(&ca_handle.cert), None))?;
cert_builder.append_extension(subject_alt_name)?;
cert_builder.sign(&ca_handle.key, hash::MessageDigest::sha256())?;
let int_cert = cert_builder.build();
Ok(CertHandle {
key: int_key,
cert: int_cert,
chain: vec![ca_handle.cert.clone()],
})
}

View file

@ -32,6 +32,7 @@ pub mod https;
mod interval;
mod ldaps;
use std::path::Path;
use std::sync::Arc;
use compact_jwt::JwsSigner;
@ -51,7 +52,6 @@ use tokio::sync::broadcast;
use crate::actors::v1_read::QueryServerReadV1;
use crate::actors::v1_write::QueryServerWriteV1;
use crate::config::Configuration;
use crate::crypto::setup_tls;
use crate::interval::IntervalActor;
// === internal setup helpers
@ -553,6 +553,98 @@ pub async fn recover_account_core(config: &Configuration, name: &str) {
);
}
pub fn cert_generate_core(config: &Configuration) {
// Get the cert root
let (tls_key_path, tls_chain_path) = match &config.tls_config {
Some(tls_config) => (
Path::new(tls_config.key.as_str()),
Path::new(tls_config.chain.as_str()),
),
None => {
error!("Unable to find tls configuration");
std::process::exit(1);
}
};
if tls_key_path.exists() && tls_chain_path.exists() {
info!(
"tls key and chain already exist - remove them first if you intend to regenerate these"
);
return;
}
let origin = match Url::parse(&config.origin) {
Ok(url) => url,
Err(e) => {
error!(err = ?e, "Unable to parse origin URL - refusing to start. You must correct the value for origin. {:?}", config.origin);
std::process::exit(1);
}
};
let origin_domain = if let Some(d) = origin.domain() {
d
} else {
error!("origin does not contain a valid domain");
std::process::exit(1);
};
let cert_root = match tls_key_path.parent() {
Some(parent) => parent,
None => {
error!("Unable to find parent directory of {:?}", tls_key_path);
std::process::exit(1);
}
};
let ca_cert = cert_root.join("ca.pem");
let ca_key = cert_root.join("cakey.pem");
let tls_cert_path = cert_root.join("cert.pem");
let ca_handle = if !ca_cert.exists() || !ca_key.exists() {
// Generate the CA again.
let ca_handle = match crypto::build_ca() {
Ok(ca_handle) => ca_handle,
Err(e) => {
error!(err = ?e, "Failed to build CA");
std::process::exit(1);
}
};
if crypto::write_ca(ca_key, ca_cert, &ca_handle).is_err() {
error!("Failed to write CA");
std::process::exit(1);
}
ca_handle
} else {
match crypto::load_ca(ca_key, ca_cert) {
Ok(ca_handle) => ca_handle,
Err(_) => {
error!("Failed to load CA");
std::process::exit(1);
}
}
};
if !tls_key_path.exists() || !tls_chain_path.exists() || !tls_cert_path.exists() {
// Generate the cert from the ca.
let cert_handle = match crypto::build_cert(origin_domain, &ca_handle) {
Ok(cert_handle) => cert_handle,
Err(e) => {
error!(err = ?e, "Failed to build certificate");
std::process::exit(1);
}
};
if crypto::write_cert(tls_key_path, tls_chain_path, tls_cert_path, &cert_handle).is_err() {
error!("Failed to write certificates");
std::process::exit(1);
}
}
info!("certificate generation complete");
}
#[derive(Clone, Debug)]
pub enum CoreAction {
Shutdown,
@ -626,7 +718,7 @@ pub async fn create_server_core(
let status_ref = StatusActor::start();
// Setup TLS (if any)
let _opt_tls_params = match setup_tls(&config) {
let _opt_tls_params = match crypto::setup_tls(&config) {
Ok(opt_tls_params) => opt_tls_params,
Err(e) => {
error!("Failed to configure TLS parameters -> {:?}", e);
@ -784,7 +876,7 @@ pub async fn create_server_core(
// If we have been requested to init LDAP, configure it now.
let maybe_ldap_acceptor_handle = match &config.ldapaddress {
Some(la) => {
let opt_ldap_tls_params = match setup_tls(&config) {
let opt_ldap_tls_params = match crypto::setup_tls(&config) {
Ok(t) => t,
Err(e) => {
error!("Failed to configure LDAP TLS parameters -> {:?}", e);

View file

@ -2,10 +2,6 @@
# This script based on the developer readme and allows you to run a test server.
if [ -z "$KANI_TMP" ]; then
KANI_TMP=/tmp/kanidm
fi
if [ -z "$KANI_CARGO_OPTS" ]; then
KANI_CARGO_OPTS=""
fi
@ -17,14 +13,9 @@ if [ ! -f "${CONFIG_FILE}" ]; then
echo "Couldn't find configuration file at ${CONFIG_FILE}, please ensure you're running this script from its base directory (${SCRIPT_DIR})."
exit 1
fi
if [ ! -f "${KANI_TMP}/chain.pem" ]; then
echo "Couldn't find certificate at /tmp/kanidm/chain.pem, quitting"
exit 1
fi
if [ ! -f "${KANI_TMP}/key.pem" ]; then
echo "Couldn't find key file at /tmp/kanidm/key.pem, quitting"
exit 1
fi
#shellcheck disable=SC2086
cargo run ${KANI_CARGO_OPTS} --bin kanidmd -- cert-generate -c "${CONFIG_FILE}"
COMMAND="server"
if [ -n "${1}" ]; then

View file

@ -25,10 +25,10 @@ use std::process::ExitCode;
use clap::{Args, Parser, Subcommand};
use kanidmd_core::config::{Configuration, ServerConfig};
use kanidmd_core::{
backup_server_core, create_server_core, dbscan_get_id2entry_core, dbscan_list_id2entry_core,
dbscan_list_index_analysis_core, dbscan_list_index_core, dbscan_list_indexes_core,
domain_rename_core, recover_account_core, reindex_server_core, restore_server_core,
vacuum_server_core, verify_server_core,
backup_server_core, cert_generate_core, create_server_core, dbscan_get_id2entry_core,
dbscan_list_id2entry_core, dbscan_list_index_analysis_core, dbscan_list_index_core,
dbscan_list_indexes_core, domain_rename_core, recover_account_core, reindex_server_core,
restore_server_core, vacuum_server_core, verify_server_core,
};
use sketching::tracing_forest::traits::*;
use sketching::tracing_forest::util::*;
@ -44,6 +44,7 @@ impl KanidmdOpt {
fn commonopt(&self) -> &CommonOpt {
match self {
KanidmdOpt::Server(sopt)
| KanidmdOpt::CertGenerate(sopt)
| KanidmdOpt::ConfigTest(sopt)
| KanidmdOpt::DbScan {
commands: DbScanOpt::ListIndexes(sopt),
@ -148,7 +149,18 @@ async fn main() -> ExitCode {
// Check the permissions are OK.
#[cfg(target_family = "unix")]
{
let cfg_path = &opt.commands.commonopt().config_path;
let cfg_path = &opt.commands.commonopt().config_path; // TODO: this needs to be pulling from the default or something?
if format!("{}", cfg_path.display()) == "".to_string() {
error!("Refusing to run - config file path is empty");
return ExitCode::FAILURE
}
if !cfg_path.exists() {
error!(
"Refusing to run - config file {} does not exist",
cfg_path.to_str().unwrap_or("invalid file path")
);
return ExitCode::FAILURE
}
let cfg_meta = match metadata(cfg_path) {
Ok(m) => m,
Err(e) => {
@ -381,8 +393,11 @@ async fn main() -> ExitCode {
}
info!("Stopped 🛑 ");
}
}
KanidmdOpt::CertGenerate(_sopt) => {
info!("Running in certificate generate mode ...");
config.update_config_for_server_mode(&sconfig);
cert_generate_core(&config);
}
KanidmdOpt::Database {
commands: DbCommands::Backup(bopt),

View file

@ -136,6 +136,11 @@ enum KanidmdOpt {
#[clap(name = "configtest")]
/// Test the IDM Server configuration, without starting network listeners.
ConfigTest(CommonOpt),
#[clap(name = "cert-generate")]
/// Create a self-signed ca and tls certificate in the locations listed from the
/// configuration. These certificates should *not* be used in production, they
/// are for testing and evaluation only!
CertGenerate(CommonOpt),
#[clap(name = "recover-account")]
/// Recover an account's password
RecoverAccount(RecoverAccountOpt),