Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

yank kube support #105

Merged
merged 1 commit into from
Jan 29, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 7 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Faythe

Tool for monitoring, issuing and persisting x509 certificates for Kubernetes HTTPS-ingress services, using Let's Encrypt.
Tool for monitoring, issuing and persisting x509 certificates, using Let's Encrypt.

## Configuration

Expand All @@ -9,30 +9,21 @@ A sample configuration file can be found at `/config.json` in this repo.

Available config options:

**kubeconfig_path** Path to kubeconfig file for connecting to and authing with the kubernetes apiserver.
This option is currently not used and might be removed. Faythe honors the KUBECONFIG env var instead.

**secret_namespace** Kubernetes Namespace in which to search and persist certificates and private keys.

**secret_hostlabel** Kubernetes Label for Secrets in which to store the hostname covered by a certificate.

**lets_encrypt_url** Which URL to use for Let's Encrypt communication. See for example: https://letsencrypt.org/docs/staging-environment/

**lets_encrypt_email** Faythe uses anonymous accounts with Let's Encrypt (meaning no auth is performed),
but some e-mail address identifying the client is still required.

**auth_dns_server** The address of the DNS-server to use for DDNS update requests.

**auth_dns_zone** The DNS-zone to use for DDNS update requests. Any ingress domain listed in Kubernetes *must* be a
subdomain of this zone. E.g. if `auth_dns_zone=goodexample.com` then Faythe will issue certs for `myingress.goodexample.com`, but
will silently ignore `myingress.badexample.com`.
**auth_dns_zone** The DNS-zone to use for DDNS update requests.

**auth_dns_key** Path to an nsupdate compatible private key to use for authing DDNS-requests at `auth_dns_server`.
**auth_dns_key** Path to an nsupdate compatible private key to use for authing DDNS-requests at `auth_dns_server`.

**val_dns_servers** List of external DNS-servers to use for validating that new DNS-records have propagated correctly.
Should preferably be set to a server which is further away (net topology-wise) than `auth_dns_server`.

**monitor_interval** The interval (in milliseconds) between each re-sync of data from the Kubernetes API.
**monitor_interval** The interval (in milliseconds) between checks of data
default: 5000 (5 seconds).

**renewal_threshold** The time (in days) before expiry of a certificate, which Faythe must start attempts to renew the cert.
Expand All @@ -45,33 +36,21 @@ default: 28800000 (8 hours)
**issue_wildcard_certs** Whether to issue wildcard certificates. (true/false)
default: false

**wildcard_cert_k8s_prefix** The name prefix to use for wildcard certificates in Kubernetes, e.g. (prefix).wildcardexample.com.
default: "wild--card"

## Design

As of writing, Faythe workload is divided into two chunks, Monitoring and Issuing.

### Monitoring
Monitoring is the process of querying the Kubernetes API to get a view of all Ingress resources in the cluster across all namespaces,
as well as retrieving all Secret objects within the namespace where Faythe stores certificates (see "Configuration -> secret_namespace").

The purpose of the Ingress resource is to declarative state which public hostnames to enable HTTP-ingress for,
as well as declare what backends to forward HTTP-requests to. Faythe has *nothing* to do with the actual proxying
of requests - but it assumes that for every hostname stated in any ingress manifest, a matching TLS-certificate is wanted.

For-each unique ingress hostname, Faythe investigates whether a matching Kubernetes Secret exists (see "Configuration -> secret_hostlabel").
If it exists, it is intended to contain two data entries: "cert" and "key". The "cert"-entry must contain a PEM-encoded x509 certificate,
which is valid until at least "today"+"renewal_threshold" (see "Configuration -> renewal_threshold").
Monitoring is the process of comparing requested certs to existing certs.

If the above criteria are not met, a request for issuing is send to the Issuer-thread.
If issuance needs to happen, eg. due to expiry, a request for issuing is send to the Issuer-thread.

### Issuing
The actual issuing of certificates involves three high level steps:

1. Ask Let's encrypt for authentication on domain ownership
2. Let's encrypt issues a challenge which must be published to the internet for LE to validate
3. When LE approves the challenge response, the cert is issued and stored as a Kubernetes Secret.
3. When LE approves the challenge response, the cert is issued and stored.

RE 2) Let's encrypt returns a challenge string which Faythe inserts via nsupdate as a TXT-record into "auth_dns_zone".
The record takes the form of `_acme-challenge.<host> IN 120 TXT <challenge>`. Before inserting the record into the DNS-zone
Expand Down
1 change: 0 additions & 1 deletion flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,6 @@
rustc
zlib.dev
dnsutils # runtime
kubectl # runtime
];
};
};
Expand Down
84 changes: 13 additions & 71 deletions src/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,9 @@ use self::openssl::x509::{X509NameEntryRef, X509};
use crate::config::VaultPersistSpec;
use crate::config::{ConfigContainer, FaytheConfig, Zone};
use crate::file::FileError;
use crate::kube::KubeError;
use crate::vault;
use crate::vault::VaultError;
use crate::{file, kube, log};
use crate::{file, log};
use acme_lib::order::NewOrder;
use acme_lib::persist::Persist;
use acme_lib::{Account, Certificate};
Expand Down Expand Up @@ -159,14 +158,6 @@ pub trait Persistable {
async fn persist(&self, cert: Certificate) -> Result<(), PersistError>;
}

#[derive(Debug, Clone, Serialize)]
pub struct KubernetesPersistSpec {
pub name: String,
pub namespace: String,
pub host_label_key: String,
pub host_label_value: String
}

#[derive(Debug, Clone, Serialize)]
pub struct FilePersistSpec {
pub private_key_path: PathBuf,
Expand All @@ -175,7 +166,6 @@ pub struct FilePersistSpec {

#[derive(Debug, Clone, Serialize)]
pub enum PersistSpec {
KUBERNETES(KubernetesPersistSpec),
FILE(FilePersistSpec),
VAULT(VaultPersistSpec),
#[allow(dead_code)]
Expand All @@ -185,7 +175,6 @@ pub enum PersistSpec {
impl Persistable for CertSpec {
async fn persist(&self, cert: Certificate) -> Result<(), PersistError> {
match &self.persist_spec {
PersistSpec::KUBERNETES(spec) => Ok(kube::persist(&spec, &cert)?),
PersistSpec::FILE(spec) => Ok(file::persist(&spec, &cert)?),
PersistSpec::VAULT(spec) => Ok(vault::persist(&spec, cert).await?),
//PersistSpec::FILE(_spec) => { unimplemented!() },
Expand All @@ -194,12 +183,6 @@ impl Persistable for CertSpec {
}
}

impl std::convert::From<KubeError> for PersistError {
fn from(err: KubeError) -> Self {
PersistError::Kube(err)
}
}

pub enum TimeError {
Diff(openssl::error::ErrorStack),
UnixTimestampOutOfBounds,
Expand Down Expand Up @@ -301,7 +284,6 @@ impl Cert {
}

pub enum PersistError {
Kube(KubeError),
File(FileError),
Vault(VaultError),
}
Expand Down Expand Up @@ -391,12 +373,11 @@ impl std::convert::From<TimeError> for CertState {
pub mod tests {

use super::*;
use crate::kube::Ingress;
use crate::file::FileSpec;
use std::collections::HashMap;
use crate::set;
use super::DNSName;
use crate::config::{ChallengeDriver, KubeMonitorConfig, FileMonitorConfig, MonitorConfig};
use crate::config::{ChallengeDriver, FileMonitorConfig, MonitorConfig};
use chrono::DateTime;

const TIME_FORMAT: &str = "%Y-%m-%dT%H:%M:%S%z"; // 2019-10-09T11:50:22+0200
Expand All @@ -410,7 +391,6 @@ pub mod tests {
let zones = create_zones(issue_wildcard_certs);
let faythe_config = FaytheConfig{
metrics_port: 9105,
kube_monitor_configs: vec!(),
file_monitor_configs: vec![file_monitor_configs.clone()],
vault_monitor_configs: vec![],
lets_encrypt_url: String::new(),
Expand Down Expand Up @@ -452,35 +432,6 @@ pub mod tests {
zones
}

pub fn create_test_kubernetes_config(issue_wildcard_certs: bool) -> ConfigContainer {
let kube_monitor_config = KubeMonitorConfig {
secret_namespace: String::new(),
secret_hostlabel: String::new(),
touch_annotation: None,
wildcard_cert_prefix: String::from("wild---card")
};
let zones = create_zones(issue_wildcard_certs);
let faythe_config = FaytheConfig{
metrics_port: 9105,
kube_monitor_configs: vec![kube_monitor_config.clone()],
file_monitor_configs: vec![],
vault_monitor_configs: vec![],
lets_encrypt_url: String::new(),
lets_encrypt_proxy: None,
lets_encrypt_email: String::new(),
val_dns_servers: Vec::new(),
monitor_interval: 0,
renewal_threshold: 30,
issue_grace: 0,
zones
};

ConfigContainer{
faythe_config,
monitor_config: MonitorConfig::Kube(kube_monitor_config)
}
}

fn create_test_certspec(cn: &str, sans: HashSet<String>) -> CertSpec {

let name = cn.to_string();
Expand All @@ -496,15 +447,6 @@ pub mod tests {
}
}

fn create_ingress(host: &str) -> Ingress {
Ingress{
name: "test".to_string(),
namespace: "test".to_string(),
touched: DateTime::<Utc>::MIN_UTC,
hosts: [host.to_string()].to_vec(),
}
}

fn create_filespec(host: &str) -> FileSpec {
FileSpec{
name: "test".to_string(),
Expand Down Expand Up @@ -534,7 +476,7 @@ pub mod tests {
assert_eq!(cert.valid_from, DateTime::parse_from_str("2020-12-01T11:42:07+0000", TIME_FORMAT).unwrap());
assert_eq!(cert.valid_to, DateTime::parse_from_str("2050-11-24T11:42:07+0000", TIME_FORMAT).unwrap());

let config = create_test_kubernetes_config(false).faythe_config;
let config = create_test_file_config(false).faythe_config;
let spec = create_test_certspec(cn, sans);

assert_eq!(cert.state(&config, &spec), CertState::Valid);
Expand All @@ -556,7 +498,7 @@ pub mod tests {
assert_eq!(cert.valid_from, DateTime::parse_from_str("2020-12-01T11:42:07+0000", TIME_FORMAT).unwrap());
assert_eq!(cert.valid_to, DateTime::parse_from_str("2050-11-24T11:42:07+0000", TIME_FORMAT).unwrap());

let config = create_test_kubernetes_config(false).faythe_config;
let config = create_test_file_config(false).faythe_config;
let spec = create_test_certspec(cn, sans);

assert_eq!(cert.state(&config, &spec), CertState::CNDoesntMatch);
Expand All @@ -577,31 +519,31 @@ pub mod tests {

let cn = "cn.longlived";
let sans = set![cn, "san1.longlived", "san2.shortlived"];
let config = create_test_kubernetes_config(false).faythe_config;
let config = create_test_file_config(false).faythe_config;
let spec = create_test_certspec(cn, sans);

assert_eq!(cert.state(&config, &spec), CertState::SANSDontMatch);
assert!(!cert.is_valid(&config, &spec));

let cn = "cn.longlived";
let sans = set![cn, "san1.longlived", "san2.longlived", "san3.longlived"];
let config = create_test_kubernetes_config(false).faythe_config;
let config = create_test_file_config(false).faythe_config;
let spec = create_test_certspec(cn, sans);

assert_eq!(cert.state(&config, &spec), CertState::SANSDontMatch);
assert!(!cert.is_valid(&config, &spec));

let cn = "cn.longlived";
let sans = set!["san2.longlived", "san1.longlived", cn]; // order of sans doesn't matter
let config = create_test_kubernetes_config(false).faythe_config;
let config = create_test_file_config(false).faythe_config;
let spec = create_test_certspec(cn, sans);

assert_eq!(cert.state(&config, &spec), CertState::Valid);
assert!(cert.is_valid(&config, &spec));

let cn = "cn.longlived";
let sans = set![cn, "san2.longlived", "san1.longlived", "san2.longlived"]; // same san can be passed multiple times to the san set
let config = create_test_kubernetes_config(false).faythe_config;
let config = create_test_file_config(false).faythe_config;
let spec = create_test_certspec(cn, sans);

assert_eq!(cert.state(&config, &spec), CertState::Valid);
Expand All @@ -626,7 +568,7 @@ pub mod tests {
assert_eq!(cert.valid_from, DateTime::parse_from_str("2020-12-01T11:41:19+0000", TIME_FORMAT).unwrap());
assert_eq!(cert.valid_to, DateTime::parse_from_str("2020-12-02T11:41:19+0000", TIME_FORMAT).unwrap());

let config = create_test_kubernetes_config(false).faythe_config;
let config = create_test_file_config(false).faythe_config;
let spec = create_test_certspec(cn, sans);

assert!(cert.state(&config, &spec) == CertState::ExpiresSoon || cert.state(&config, &spec) == CertState::Expired);
Expand All @@ -636,7 +578,7 @@ pub mod tests {
#[test]
fn test_find_zone() {
{
let config = create_test_kubernetes_config(false);
let config = create_test_file_config(false);

let host: DNSName = DNSName::try_from(&String::from("host1.subdivision.unit.wrongtest")).unwrap();
let zone = host.find_zone(&config.faythe_config);
Expand All @@ -660,7 +602,7 @@ pub mod tests {
}

{
let config = create_test_kubernetes_config(false);
let config = create_test_file_config(false);

let host: DNSName = DNSName::try_from(&String::from("host1.subdivision.unit.test")).unwrap();
let zone = host.find_zone(&config.faythe_config).unwrap();
Expand Down Expand Up @@ -691,9 +633,9 @@ pub mod tests {
assert_eq!(cert.cn, cn);
assert_eq!(cert.sans, sans);

let container = create_test_kubernetes_config(true);
let container = create_test_file_config(true);
let config = &container.faythe_config;
let spec = create_ingress("foo.unit.test").to_cert_spec(&container).unwrap();
let spec = create_filespec("foo.unit.test").to_cert_spec(&container).unwrap();

assert!(cert.state(&config, &spec) == CertState::Valid);
assert!(cert.is_valid(&config, &spec));
Expand Down
20 changes: 0 additions & 20 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,24 +27,11 @@ pub struct FaytheConfig {
#[serde(default = "default_issue_grace")]
pub issue_grace: u64,
#[serde(default)]
pub kube_monitor_configs: Vec<KubeMonitorConfig>,
#[serde(default)]
pub file_monitor_configs: Vec<FileMonitorConfig>,
#[serde(default)]
pub vault_monitor_configs: Vec<VaultMonitorConfig>,
}

#[derive(Clone, Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct KubeMonitorConfig {
pub secret_namespace: String,
pub secret_hostlabel: String,
#[serde(default = "default_wildcard_cert_k8s_prefix")]
pub wildcard_cert_prefix: String,
#[serde(default = "default_k8s_touch_annotation")]
pub touch_annotation: Option<String>,
}

#[derive(Clone, Deserialize, Serialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct VaultMonitorConfig {
Expand Down Expand Up @@ -78,7 +65,6 @@ pub struct FileMonitorConfig {
}

pub enum MonitorConfig {
Kube(KubeMonitorConfig),
File(FileMonitorConfig),
Vault(VaultMonitorConfig),
}
Expand Down Expand Up @@ -128,12 +114,6 @@ fn default_timeout_secs() -> u8 {
}

impl ConfigContainer {
pub fn get_kube_monitor_config(&self) -> Result<&KubeMonitorConfig, SpecError> {
Ok(match &self.monitor_config {
MonitorConfig::Kube(c) => Ok(c),
_ => Err(SpecError::InvalidConfig)
}?)
}
pub fn get_file_monitor_config(&self) -> Result<&FileMonitorConfig, SpecError> {
Ok(match &self.monitor_config {
MonitorConfig::File(c) => Ok(c),
Expand Down
Loading