mirror of
https://github.com/neondatabase/neon.git
synced 2025-12-25 23:29:59 +00:00
Support audit syslog over TLS (#12124)
Add support to transport syslogs over TLS. Since TLS params essentially require passing host and port separately, add a boolean flag to the configuration template and also use the same `action` format for plaintext logs. This allows seamless transition. The plaintext host:port is picked from `AUDIT_LOGGING_ENDPOINT` (as earlier) and from `AUDIT_LOGGING_TLS_ENDPOINT`. The TLS host:port is used when defined and non-empty. `remote_endpoint` is split separately to hostname and port as required by `omfwd` module. Also the address parsing and config content generation are split to more testable functions with basic tests added.
This commit is contained in:
7
Cargo.lock
generated
7
Cargo.lock
generated
@@ -1305,6 +1305,7 @@ dependencies = [
|
|||||||
"fail",
|
"fail",
|
||||||
"flate2",
|
"flate2",
|
||||||
"futures",
|
"futures",
|
||||||
|
"hostname-validator",
|
||||||
"http 1.1.0",
|
"http 1.1.0",
|
||||||
"indexmap 2.9.0",
|
"indexmap 2.9.0",
|
||||||
"itertools 0.10.5",
|
"itertools 0.10.5",
|
||||||
@@ -2771,6 +2772,12 @@ dependencies = [
|
|||||||
"windows",
|
"windows",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hostname-validator"
|
||||||
|
version = "1.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f558a64ac9af88b5ba400d99b579451af0d39c6d360980045b91aac966d705e2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "http"
|
name = "http"
|
||||||
version = "0.2.9"
|
version = "0.2.9"
|
||||||
|
|||||||
@@ -1983,7 +1983,7 @@ RUN apt update && \
|
|||||||
locales \
|
locales \
|
||||||
lsof \
|
lsof \
|
||||||
procps \
|
procps \
|
||||||
rsyslog \
|
rsyslog-gnutls \
|
||||||
screen \
|
screen \
|
||||||
tcpdump \
|
tcpdump \
|
||||||
$VERSION_INSTALLS && \
|
$VERSION_INSTALLS && \
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ fail.workspace = true
|
|||||||
flate2.workspace = true
|
flate2.workspace = true
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
http.workspace = true
|
http.workspace = true
|
||||||
|
hostname-validator = "1.1"
|
||||||
indexmap.workspace = true
|
indexmap.workspace = true
|
||||||
itertools.workspace = true
|
itertools.workspace = true
|
||||||
jsonwebtoken.workspace = true
|
jsonwebtoken.workspace = true
|
||||||
|
|||||||
@@ -759,10 +759,15 @@ impl ComputeNode {
|
|||||||
// Configure and start rsyslog for compliance audit logging
|
// Configure and start rsyslog for compliance audit logging
|
||||||
match pspec.spec.audit_log_level {
|
match pspec.spec.audit_log_level {
|
||||||
ComputeAudit::Hipaa | ComputeAudit::Extended | ComputeAudit::Full => {
|
ComputeAudit::Hipaa | ComputeAudit::Extended | ComputeAudit::Full => {
|
||||||
let remote_endpoint =
|
let remote_tls_endpoint =
|
||||||
|
std::env::var("AUDIT_LOGGING_TLS_ENDPOINT").unwrap_or("".to_string());
|
||||||
|
let remote_plain_endpoint =
|
||||||
std::env::var("AUDIT_LOGGING_ENDPOINT").unwrap_or("".to_string());
|
std::env::var("AUDIT_LOGGING_ENDPOINT").unwrap_or("".to_string());
|
||||||
if remote_endpoint.is_empty() {
|
|
||||||
anyhow::bail!("AUDIT_LOGGING_ENDPOINT is empty");
|
if remote_plain_endpoint.is_empty() && remote_tls_endpoint.is_empty() {
|
||||||
|
anyhow::bail!(
|
||||||
|
"AUDIT_LOGGING_ENDPOINT and AUDIT_LOGGING_TLS_ENDPOINT are both empty"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let log_directory_path = Path::new(&self.params.pgdata).join("log");
|
let log_directory_path = Path::new(&self.params.pgdata).join("log");
|
||||||
@@ -778,7 +783,8 @@ impl ComputeNode {
|
|||||||
log_directory_path.clone(),
|
log_directory_path.clone(),
|
||||||
endpoint_id,
|
endpoint_id,
|
||||||
project_id,
|
project_id,
|
||||||
&remote_endpoint,
|
&remote_plain_endpoint,
|
||||||
|
&remote_tls_endpoint,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// Launch a background task to clean up the audit logs
|
// Launch a background task to clean up the audit logs
|
||||||
|
|||||||
@@ -10,7 +10,13 @@ input(type="imfile" File="{log_directory}/*.log"
|
|||||||
startmsg.regex="^[[:digit:]]{{4}}-[[:digit:]]{{2}}-[[:digit:]]{{2}} [[:digit:]]{{2}}:[[:digit:]]{{2}}:[[:digit:]]{{2}}.[[:digit:]]{{3}} GMT,")
|
startmsg.regex="^[[:digit:]]{{4}}-[[:digit:]]{{2}}-[[:digit:]]{{2}} [[:digit:]]{{2}}:[[:digit:]]{{2}}:[[:digit:]]{{2}}.[[:digit:]]{{3}} GMT,")
|
||||||
|
|
||||||
# the directory to store rsyslog state files
|
# the directory to store rsyslog state files
|
||||||
global(workDirectory="/var/log/rsyslog")
|
global(
|
||||||
|
workDirectory="/var/log/rsyslog"
|
||||||
|
DefaultNetstreamDriverCAFile="/etc/ssl/certs/ca-certificates.crt"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Whether the remote syslog receiver uses tls
|
||||||
|
set $.remote_syslog_tls = "{remote_syslog_tls}";
|
||||||
|
|
||||||
# Construct json, endpoint_id and project_id as additional metadata
|
# Construct json, endpoint_id and project_id as additional metadata
|
||||||
set $.json_log!endpoint_id = "{endpoint_id}";
|
set $.json_log!endpoint_id = "{endpoint_id}";
|
||||||
@@ -21,5 +27,29 @@ set $.json_log!msg = $msg;
|
|||||||
template(name="PgAuditLog" type="string"
|
template(name="PgAuditLog" type="string"
|
||||||
string="<%PRI%>1 %TIMESTAMP:::date-rfc3339% %HOSTNAME% - - - - %$.json_log%")
|
string="<%PRI%>1 %TIMESTAMP:::date-rfc3339% %HOSTNAME% - - - - %$.json_log%")
|
||||||
|
|
||||||
# Forward to remote syslog receiver (@@<hostname>:<port>;format
|
# Forward to remote syslog receiver (over TLS)
|
||||||
local5.info @@{remote_endpoint};PgAuditLog
|
if ( $syslogtag == 'pgaudit_log' ) then {{
|
||||||
|
if ( $.remote_syslog_tls == 'true' ) then {{
|
||||||
|
action(type="omfwd" target="{remote_syslog_host}" port="{remote_syslog_port}" protocol="tcp"
|
||||||
|
template="PgAuditLog"
|
||||||
|
queue.type="linkedList"
|
||||||
|
queue.size="1000"
|
||||||
|
action.ResumeRetryCount="10"
|
||||||
|
StreamDriver="gtls"
|
||||||
|
StreamDriverMode="1"
|
||||||
|
StreamDriverAuthMode="x509/name"
|
||||||
|
StreamDriverPermittedPeers="{remote_syslog_host}"
|
||||||
|
StreamDriver.CheckExtendedKeyPurpose="on"
|
||||||
|
StreamDriver.PermitExpiredCerts="off"
|
||||||
|
)
|
||||||
|
stop
|
||||||
|
}} else {{
|
||||||
|
action(type="omfwd" target="{remote_syslog_host}" port="{remote_syslog_port}" protocol="tcp"
|
||||||
|
template="PgAuditLog"
|
||||||
|
queue.type="linkedList"
|
||||||
|
queue.size="1000"
|
||||||
|
action.ResumeRetryCount="10"
|
||||||
|
)
|
||||||
|
stop
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ use std::path::Path;
|
|||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use std::{fs::OpenOptions, io::Write};
|
use std::{fs::OpenOptions, io::Write};
|
||||||
|
use url::{Host, Url};
|
||||||
|
|
||||||
use anyhow::{Context, Result, anyhow};
|
use anyhow::{Context, Result, anyhow};
|
||||||
|
use hostname_validator;
|
||||||
use tracing::{error, info, instrument, warn};
|
use tracing::{error, info, instrument, warn};
|
||||||
|
|
||||||
const POSTGRES_LOGS_CONF_PATH: &str = "/etc/rsyslog.d/postgres_logs.conf";
|
const POSTGRES_LOGS_CONF_PATH: &str = "/etc/rsyslog.d/postgres_logs.conf";
|
||||||
@@ -82,18 +84,84 @@ fn restart_rsyslog() -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_audit_syslog_address(
|
||||||
|
remote_plain_endpoint: &str,
|
||||||
|
remote_tls_endpoint: &str,
|
||||||
|
) -> Result<(String, u16, String)> {
|
||||||
|
let tls;
|
||||||
|
let remote_endpoint = if !remote_tls_endpoint.is_empty() {
|
||||||
|
tls = "true".to_string();
|
||||||
|
remote_tls_endpoint
|
||||||
|
} else {
|
||||||
|
tls = "false".to_string();
|
||||||
|
remote_plain_endpoint
|
||||||
|
};
|
||||||
|
// Urlify the remote_endpoint, so parsing can be done with url::Url.
|
||||||
|
let url_str = format!("http://{remote_endpoint}");
|
||||||
|
let url = Url::parse(&url_str).map_err(|err| {
|
||||||
|
anyhow!("Error parsing {remote_endpoint}, expected host:port, got {err:?}")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let is_valid = url.scheme() == "http"
|
||||||
|
&& url.path() == "/"
|
||||||
|
&& url.query().is_none()
|
||||||
|
&& url.fragment().is_none()
|
||||||
|
&& url.username() == ""
|
||||||
|
&& url.password().is_none();
|
||||||
|
|
||||||
|
if !is_valid {
|
||||||
|
return Err(anyhow!(
|
||||||
|
"Invalid address format {remote_endpoint}, expected host:port"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let host = match url.host() {
|
||||||
|
Some(Host::Domain(h)) if hostname_validator::is_valid(h) => h.to_string(),
|
||||||
|
Some(Host::Ipv4(ip4)) => ip4.to_string(),
|
||||||
|
Some(Host::Ipv6(ip6)) => ip6.to_string(),
|
||||||
|
_ => return Err(anyhow!("Invalid host")),
|
||||||
|
};
|
||||||
|
let port = url
|
||||||
|
.port()
|
||||||
|
.ok_or_else(|| anyhow!("Invalid port in {remote_endpoint}"))?;
|
||||||
|
|
||||||
|
Ok((host, port, tls))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_audit_rsyslog_config(
|
||||||
|
log_directory: String,
|
||||||
|
endpoint_id: &str,
|
||||||
|
project_id: &str,
|
||||||
|
remote_syslog_host: &str,
|
||||||
|
remote_syslog_port: u16,
|
||||||
|
remote_syslog_tls: &str,
|
||||||
|
) -> String {
|
||||||
|
format!(
|
||||||
|
include_str!("config_template/compute_audit_rsyslog_template.conf"),
|
||||||
|
log_directory = log_directory,
|
||||||
|
endpoint_id = endpoint_id,
|
||||||
|
project_id = project_id,
|
||||||
|
remote_syslog_host = remote_syslog_host,
|
||||||
|
remote_syslog_port = remote_syslog_port,
|
||||||
|
remote_syslog_tls = remote_syslog_tls
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn configure_audit_rsyslog(
|
pub fn configure_audit_rsyslog(
|
||||||
log_directory: String,
|
log_directory: String,
|
||||||
endpoint_id: &str,
|
endpoint_id: &str,
|
||||||
project_id: &str,
|
project_id: &str,
|
||||||
remote_endpoint: &str,
|
remote_endpoint: &str,
|
||||||
|
remote_tls_endpoint: &str,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let config_content: String = format!(
|
let (remote_syslog_host, remote_syslog_port, remote_syslog_tls) =
|
||||||
include_str!("config_template/compute_audit_rsyslog_template.conf"),
|
parse_audit_syslog_address(remote_endpoint, remote_tls_endpoint).unwrap();
|
||||||
log_directory = log_directory,
|
let config_content = generate_audit_rsyslog_config(
|
||||||
endpoint_id = endpoint_id,
|
log_directory,
|
||||||
project_id = project_id,
|
endpoint_id,
|
||||||
remote_endpoint = remote_endpoint
|
project_id,
|
||||||
|
&remote_syslog_host,
|
||||||
|
remote_syslog_port,
|
||||||
|
&remote_syslog_tls,
|
||||||
);
|
);
|
||||||
|
|
||||||
info!("rsyslog config_content: {}", config_content);
|
info!("rsyslog config_content: {}", config_content);
|
||||||
@@ -258,6 +326,8 @@ pub fn launch_pgaudit_gc(log_directory: String) {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use crate::rsyslog::PostgresLogsRsyslogConfig;
|
use crate::rsyslog::PostgresLogsRsyslogConfig;
|
||||||
|
|
||||||
|
use super::{generate_audit_rsyslog_config, parse_audit_syslog_address};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_postgres_logs_config() {
|
fn test_postgres_logs_config() {
|
||||||
{
|
{
|
||||||
@@ -287,4 +357,146 @@ mod tests {
|
|||||||
assert!(res.is_err());
|
assert!(res.is_err());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_audit_syslog_address() {
|
||||||
|
{
|
||||||
|
// host:port format (plaintext)
|
||||||
|
let parsed = parse_audit_syslog_address("collector.host.tld:5555", "");
|
||||||
|
assert!(parsed.is_ok());
|
||||||
|
assert_eq!(
|
||||||
|
parsed.unwrap(),
|
||||||
|
(
|
||||||
|
String::from("collector.host.tld"),
|
||||||
|
5555,
|
||||||
|
String::from("false")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// host:port format with ipv4 ip address (plaintext)
|
||||||
|
let parsed = parse_audit_syslog_address("10.0.0.1:5555", "");
|
||||||
|
assert!(parsed.is_ok());
|
||||||
|
assert_eq!(
|
||||||
|
parsed.unwrap(),
|
||||||
|
(String::from("10.0.0.1"), 5555, String::from("false"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// host:port format with ipv6 ip address (plaintext)
|
||||||
|
let parsed =
|
||||||
|
parse_audit_syslog_address("[7e60:82ed:cb2e:d617:f904:f395:aaca:e252]:5555", "");
|
||||||
|
assert_eq!(
|
||||||
|
parsed.unwrap(),
|
||||||
|
(
|
||||||
|
String::from("7e60:82ed:cb2e:d617:f904:f395:aaca:e252"),
|
||||||
|
5555,
|
||||||
|
String::from("false")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// Only TLS host:port defined
|
||||||
|
let parsed = parse_audit_syslog_address("", "tls.host.tld:5556");
|
||||||
|
assert_eq!(
|
||||||
|
parsed.unwrap(),
|
||||||
|
(String::from("tls.host.tld"), 5556, String::from("true"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// tls host should take precedence, when both defined
|
||||||
|
let parsed = parse_audit_syslog_address("plaintext.host.tld:5555", "tls.host.tld:5556");
|
||||||
|
assert_eq!(
|
||||||
|
parsed.unwrap(),
|
||||||
|
(String::from("tls.host.tld"), 5556, String::from("true"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// host without port (plaintext)
|
||||||
|
let parsed = parse_audit_syslog_address("collector.host.tld", "");
|
||||||
|
assert!(parsed.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// port without host
|
||||||
|
let parsed = parse_audit_syslog_address(":5555", "");
|
||||||
|
assert!(parsed.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// valid host with invalid port
|
||||||
|
let parsed = parse_audit_syslog_address("collector.host.tld:90001", "");
|
||||||
|
assert!(parsed.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// invalid hostname with valid port
|
||||||
|
let parsed = parse_audit_syslog_address("-collector.host.tld:5555", "");
|
||||||
|
assert!(parsed.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// parse error
|
||||||
|
let parsed = parse_audit_syslog_address("collector.host.tld:::5555", "");
|
||||||
|
assert!(parsed.is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_generate_audit_rsyslog_config() {
|
||||||
|
{
|
||||||
|
// plaintext version
|
||||||
|
let log_directory = "/tmp/log".to_string();
|
||||||
|
let endpoint_id = "ep-test-endpoint-id";
|
||||||
|
let project_id = "test-project-id";
|
||||||
|
let remote_syslog_host = "collector.host.tld";
|
||||||
|
let remote_syslog_port = 5555;
|
||||||
|
let remote_syslog_tls = "false";
|
||||||
|
|
||||||
|
let conf_str = generate_audit_rsyslog_config(
|
||||||
|
log_directory,
|
||||||
|
endpoint_id,
|
||||||
|
project_id,
|
||||||
|
remote_syslog_host,
|
||||||
|
remote_syslog_port,
|
||||||
|
remote_syslog_tls,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(conf_str.contains(r#"set $.remote_syslog_tls = "false";"#));
|
||||||
|
assert!(conf_str.contains(r#"type="omfwd""#));
|
||||||
|
assert!(conf_str.contains(r#"target="collector.host.tld""#));
|
||||||
|
assert!(conf_str.contains(r#"port="5555""#));
|
||||||
|
assert!(conf_str.contains(r#"StreamDriverPermittedPeers="collector.host.tld""#));
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// TLS version
|
||||||
|
let log_directory = "/tmp/log".to_string();
|
||||||
|
let endpoint_id = "ep-test-endpoint-id";
|
||||||
|
let project_id = "test-project-id";
|
||||||
|
let remote_syslog_host = "collector.host.tld";
|
||||||
|
let remote_syslog_port = 5556;
|
||||||
|
let remote_syslog_tls = "true";
|
||||||
|
|
||||||
|
let conf_str = generate_audit_rsyslog_config(
|
||||||
|
log_directory,
|
||||||
|
endpoint_id,
|
||||||
|
project_id,
|
||||||
|
remote_syslog_host,
|
||||||
|
remote_syslog_port,
|
||||||
|
remote_syslog_tls,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(conf_str.contains(r#"set $.remote_syslog_tls = "true";"#));
|
||||||
|
assert!(conf_str.contains(r#"type="omfwd""#));
|
||||||
|
assert!(conf_str.contains(r#"target="collector.host.tld""#));
|
||||||
|
assert!(conf_str.contains(r#"port="5556""#));
|
||||||
|
assert!(conf_str.contains(r#"StreamDriverPermittedPeers="collector.host.tld""#));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user