Compare commits
9 Commits
feat/pente
...
fix/gitea-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f1bb053882 | ||
|
|
af4760baf5 | ||
|
|
f310a3e0a2 | ||
|
|
e5c14636a7 | ||
|
|
fad8bbbd65 | ||
|
|
c62e9fdcd4 | ||
| a9d039dad3 | |||
|
|
a509bdcb2e | ||
| c461faa2fb |
6
Cargo.lock
generated
6
Cargo.lock
generated
@@ -4699,9 +4699,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls-webpki"
|
name = "rustls-webpki"
|
||||||
version = "0.103.9"
|
version = "0.103.10"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53"
|
checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ring",
|
"ring",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
@@ -5171,7 +5171,7 @@ version = "0.8.9"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c1c97747dbf44bb1ca44a561ece23508e99cb592e862f22222dcf42f51d1e451"
|
checksum = "c1c97747dbf44bb1ca44a561ece23508e99cb592e862f22222dcf42f51d1e451"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck 0.5.0",
|
"heck 0.4.1",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn",
|
||||||
|
|||||||
@@ -54,6 +54,9 @@ pub fn load_config() -> Result<AgentConfig, AgentError> {
|
|||||||
pentest_verification_email: env_var_opt("PENTEST_VERIFICATION_EMAIL"),
|
pentest_verification_email: env_var_opt("PENTEST_VERIFICATION_EMAIL"),
|
||||||
pentest_imap_host: env_var_opt("PENTEST_IMAP_HOST"),
|
pentest_imap_host: env_var_opt("PENTEST_IMAP_HOST"),
|
||||||
pentest_imap_port: env_var_opt("PENTEST_IMAP_PORT").and_then(|p| p.parse().ok()),
|
pentest_imap_port: env_var_opt("PENTEST_IMAP_PORT").and_then(|p| p.parse().ok()),
|
||||||
|
pentest_imap_tls: env_var_opt("PENTEST_IMAP_TLS")
|
||||||
|
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
|
||||||
|
.unwrap_or(true),
|
||||||
pentest_imap_username: env_var_opt("PENTEST_IMAP_USERNAME"),
|
pentest_imap_username: env_var_opt("PENTEST_IMAP_USERNAME"),
|
||||||
pentest_imap_password: env_secret_opt("PENTEST_IMAP_PASSWORD"),
|
pentest_imap_password: env_secret_opt("PENTEST_IMAP_PASSWORD"),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -336,6 +336,7 @@ mod tests {
|
|||||||
pentest_verification_email: None,
|
pentest_verification_email: None,
|
||||||
pentest_imap_host: None,
|
pentest_imap_host: None,
|
||||||
pentest_imap_port: None,
|
pentest_imap_port: None,
|
||||||
|
pentest_imap_tls: true,
|
||||||
pentest_imap_username: None,
|
pentest_imap_username: None,
|
||||||
pentest_imap_password: None,
|
pentest_imap_password: None,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,8 +66,10 @@ impl CodeReviewScanner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let deduped = dedup_cross_pass(all_findings);
|
||||||
|
|
||||||
ScanOutput {
|
ScanOutput {
|
||||||
findings: all_findings,
|
findings: deduped,
|
||||||
sbom_entries: Vec::new(),
|
sbom_entries: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -184,3 +186,51 @@ struct ReviewIssue {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
suggestion: Option<String>,
|
suggestion: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Deduplicate findings across review passes.
|
||||||
|
///
|
||||||
|
/// Multiple passes often flag the same issue (e.g. SQL injection reported by
|
||||||
|
/// logic, security, and convention passes). We group by file + nearby line +
|
||||||
|
/// normalized title keywords and keep the highest-severity finding.
|
||||||
|
fn dedup_cross_pass(findings: Vec<Finding>) -> Vec<Finding> {
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
// Build a dedup key: (file, line bucket, normalized title words)
|
||||||
|
fn dedup_key(f: &Finding) -> String {
|
||||||
|
let file = f.file_path.as_deref().unwrap_or("");
|
||||||
|
// Group lines within 3 of each other
|
||||||
|
let line_bucket = f.line_number.unwrap_or(0) / 4;
|
||||||
|
// Normalize: lowercase, keep only alphanumeric, sort words for order-independence
|
||||||
|
let title_lower = f.title.to_lowercase();
|
||||||
|
let mut words: Vec<&str> = title_lower
|
||||||
|
.split(|c: char| !c.is_alphanumeric())
|
||||||
|
.filter(|w| w.len() > 2)
|
||||||
|
.collect();
|
||||||
|
words.sort();
|
||||||
|
format!("{file}:{line_bucket}:{}", words.join(","))
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut groups: HashMap<String, Finding> = HashMap::new();
|
||||||
|
|
||||||
|
for finding in findings {
|
||||||
|
let key = dedup_key(&finding);
|
||||||
|
groups
|
||||||
|
.entry(key)
|
||||||
|
.and_modify(|existing| {
|
||||||
|
// Keep the higher severity; on tie, keep the one with more detail
|
||||||
|
if finding.severity > existing.severity
|
||||||
|
|| (finding.severity == existing.severity
|
||||||
|
&& finding.description.len() > existing.description.len())
|
||||||
|
{
|
||||||
|
*existing = finding.clone();
|
||||||
|
}
|
||||||
|
// Merge CWE if the existing one is missing it
|
||||||
|
if existing.cwe.is_none() {
|
||||||
|
existing.cwe = finding.cwe.clone();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.or_insert(finding);
|
||||||
|
}
|
||||||
|
|
||||||
|
groups.into_values().collect()
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ use compliance_core::AgentConfig;
|
|||||||
use crate::database::Database;
|
use crate::database::Database;
|
||||||
use crate::error::AgentError;
|
use crate::error::AgentError;
|
||||||
use crate::llm::LlmClient;
|
use crate::llm::LlmClient;
|
||||||
use crate::pipeline::code_review::CodeReviewScanner;
|
|
||||||
use crate::pipeline::cve::CveScanner;
|
use crate::pipeline::cve::CveScanner;
|
||||||
use crate::pipeline::git::GitOps;
|
use crate::pipeline::git::GitOps;
|
||||||
use crate::pipeline::gitleaks::GitleaksScanner;
|
use crate::pipeline::gitleaks::GitleaksScanner;
|
||||||
@@ -241,21 +240,6 @@ impl PipelineOrchestrator {
|
|||||||
Err(e) => tracing::warn!("[{repo_id}] Lint scanning failed: {e}"),
|
Err(e) => tracing::warn!("[{repo_id}] Lint scanning failed: {e}"),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stage 4c: LLM Code Review (only on incremental scans)
|
|
||||||
if let Some(old_sha) = &repo.last_scanned_commit {
|
|
||||||
tracing::info!("[{repo_id}] Stage 4c: LLM Code Review");
|
|
||||||
self.update_phase(scan_run_id, "code_review").await;
|
|
||||||
let review_output = async {
|
|
||||||
let reviewer = CodeReviewScanner::new(self.llm.clone());
|
|
||||||
reviewer
|
|
||||||
.review_diff(&repo_path, &repo_id, old_sha, ¤t_sha)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
.instrument(tracing::info_span!("stage_code_review"))
|
|
||||||
.await;
|
|
||||||
all_findings.extend(review_output.findings);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stage 4.5: Graph Building
|
// Stage 4.5: Graph Building
|
||||||
tracing::info!("[{repo_id}] Stage 4.5: Graph Building");
|
tracing::info!("[{repo_id}] Stage 4.5: Graph Building");
|
||||||
self.update_phase(scan_run_id, "graph_building").await;
|
self.update_phase(scan_run_id, "graph_building").await;
|
||||||
|
|||||||
@@ -98,7 +98,8 @@ impl IssueTracker for GiteaTracker {
|
|||||||
_ => "open",
|
_ => "open",
|
||||||
};
|
};
|
||||||
|
|
||||||
self.http
|
let resp = self
|
||||||
|
.http
|
||||||
.patch(&url)
|
.patch(&url)
|
||||||
.header(
|
.header(
|
||||||
"Authorization",
|
"Authorization",
|
||||||
@@ -109,6 +110,14 @@ impl IssueTracker for GiteaTracker {
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| CoreError::IssueTracker(format!("Gitea update issue failed: {e}")))?;
|
.map_err(|e| CoreError::IssueTracker(format!("Gitea update issue failed: {e}")))?;
|
||||||
|
|
||||||
|
if !resp.status().is_success() {
|
||||||
|
let status = resp.status();
|
||||||
|
let text = resp.text().await.unwrap_or_default();
|
||||||
|
return Err(CoreError::IssueTracker(format!(
|
||||||
|
"Gitea update issue returned {status}: {text}"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,7 +132,8 @@ impl IssueTracker for GiteaTracker {
|
|||||||
"/repos/{owner}/{repo}/issues/{external_id}/comments"
|
"/repos/{owner}/{repo}/issues/{external_id}/comments"
|
||||||
));
|
));
|
||||||
|
|
||||||
self.http
|
let resp = self
|
||||||
|
.http
|
||||||
.post(&url)
|
.post(&url)
|
||||||
.header(
|
.header(
|
||||||
"Authorization",
|
"Authorization",
|
||||||
@@ -134,6 +144,14 @@ impl IssueTracker for GiteaTracker {
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| CoreError::IssueTracker(format!("Gitea add comment failed: {e}")))?;
|
.map_err(|e| CoreError::IssueTracker(format!("Gitea add comment failed: {e}")))?;
|
||||||
|
|
||||||
|
if !resp.status().is_success() {
|
||||||
|
let status = resp.status();
|
||||||
|
let text = resp.text().await.unwrap_or_default();
|
||||||
|
return Err(CoreError::IssueTracker(format!(
|
||||||
|
"Gitea add comment returned {status}: {text}"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,7 +176,8 @@ impl IssueTracker for GiteaTracker {
|
|||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
self.http
|
let resp = self
|
||||||
|
.http
|
||||||
.post(&url)
|
.post(&url)
|
||||||
.header(
|
.header(
|
||||||
"Authorization",
|
"Authorization",
|
||||||
@@ -173,6 +192,48 @@ impl IssueTracker for GiteaTracker {
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| CoreError::IssueTracker(format!("Gitea PR review failed: {e}")))?;
|
.map_err(|e| CoreError::IssueTracker(format!("Gitea PR review failed: {e}")))?;
|
||||||
|
|
||||||
|
if !resp.status().is_success() {
|
||||||
|
let status = resp.status();
|
||||||
|
let text = resp.text().await.unwrap_or_default();
|
||||||
|
|
||||||
|
// If inline comments caused the failure, retry with just the summary body
|
||||||
|
if !comments.is_empty() {
|
||||||
|
tracing::warn!(
|
||||||
|
"Gitea PR review with inline comments failed ({status}): {text}, retrying as plain comment"
|
||||||
|
);
|
||||||
|
let fallback_url = self.api_url(&format!(
|
||||||
|
"/repos/{owner}/{repo}/issues/{pr_number}/comments"
|
||||||
|
));
|
||||||
|
let fallback_resp = self
|
||||||
|
.http
|
||||||
|
.post(&fallback_url)
|
||||||
|
.header(
|
||||||
|
"Authorization",
|
||||||
|
format!("token {}", self.token.expose_secret()),
|
||||||
|
)
|
||||||
|
.json(&serde_json::json!({ "body": body }))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
CoreError::IssueTracker(format!("Gitea PR comment fallback failed: {e}"))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if !fallback_resp.status().is_success() {
|
||||||
|
let fb_status = fallback_resp.status();
|
||||||
|
let fb_text = fallback_resp.text().await.unwrap_or_default();
|
||||||
|
return Err(CoreError::IssueTracker(format!(
|
||||||
|
"Gitea PR comment fallback returned {fb_status}: {fb_text}"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
return Err(CoreError::IssueTracker(format!(
|
||||||
|
"Gitea PR review returned {status}: {text}"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ pub struct AgentConfig {
|
|||||||
pub pentest_verification_email: Option<String>,
|
pub pentest_verification_email: Option<String>,
|
||||||
pub pentest_imap_host: Option<String>,
|
pub pentest_imap_host: Option<String>,
|
||||||
pub pentest_imap_port: Option<u16>,
|
pub pentest_imap_port: Option<u16>,
|
||||||
|
/// Use implicit TLS (IMAPS, port 993) instead of plain IMAP.
|
||||||
|
pub pentest_imap_tls: bool,
|
||||||
pub pentest_imap_username: Option<String>,
|
pub pentest_imap_username: Option<String>,
|
||||||
pub pentest_imap_password: Option<SecretString>,
|
pub pentest_imap_password: Option<SecretString>,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,6 +113,72 @@ pub async fn add_mcp_server(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Probe each MCP server's health endpoint and update status in MongoDB.
|
||||||
|
#[server]
|
||||||
|
pub async fn refresh_mcp_status() -> Result<(), ServerFnError> {
|
||||||
|
use chrono::Utc;
|
||||||
|
use compliance_core::models::McpServerStatus;
|
||||||
|
use mongodb::bson::doc;
|
||||||
|
|
||||||
|
let state: super::server_state::ServerState =
|
||||||
|
dioxus_fullstack::FullstackContext::extract().await?;
|
||||||
|
|
||||||
|
let mut cursor = state
|
||||||
|
.db
|
||||||
|
.mcp_servers()
|
||||||
|
.find(doc! {})
|
||||||
|
.await
|
||||||
|
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||||
|
|
||||||
|
let client = reqwest::Client::builder()
|
||||||
|
.timeout(std::time::Duration::from_secs(5))
|
||||||
|
.build()
|
||||||
|
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||||
|
|
||||||
|
while cursor
|
||||||
|
.advance()
|
||||||
|
.await
|
||||||
|
.map_err(|e| ServerFnError::new(e.to_string()))?
|
||||||
|
{
|
||||||
|
let server: compliance_core::models::McpServerConfig = cursor
|
||||||
|
.deserialize_current()
|
||||||
|
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||||
|
|
||||||
|
let Some(oid) = server.id else { continue };
|
||||||
|
|
||||||
|
// Derive health URL from the endpoint (replace trailing /mcp with /health)
|
||||||
|
let health_url = if server.endpoint_url.ends_with("/mcp") {
|
||||||
|
format!(
|
||||||
|
"{}health",
|
||||||
|
&server.endpoint_url[..server.endpoint_url.len() - 3]
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
format!("{}/health", server.endpoint_url.trim_end_matches('/'))
|
||||||
|
};
|
||||||
|
|
||||||
|
let new_status = match client.get(&health_url).send().await {
|
||||||
|
Ok(resp) if resp.status().is_success() => McpServerStatus::Running,
|
||||||
|
_ => McpServerStatus::Stopped,
|
||||||
|
};
|
||||||
|
|
||||||
|
let status_bson = match bson::to_bson(&new_status) {
|
||||||
|
Ok(b) => b,
|
||||||
|
Err(_) => continue,
|
||||||
|
};
|
||||||
|
|
||||||
|
let _ = state
|
||||||
|
.db
|
||||||
|
.mcp_servers()
|
||||||
|
.update_one(
|
||||||
|
doc! { "_id": oid },
|
||||||
|
doc! { "$set": { "status": status_bson, "updated_at": Utc::now().to_rfc3339() } },
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[server]
|
#[server]
|
||||||
pub async fn delete_mcp_server(server_id: String) -> Result<(), ServerFnError> {
|
pub async fn delete_mcp_server(server_id: String) -> Result<(), ServerFnError> {
|
||||||
use mongodb::bson::doc;
|
use mongodb::bson::doc;
|
||||||
|
|||||||
@@ -123,7 +123,6 @@ pub fn FindingsPage() -> Element {
|
|||||||
option { value: "oauth", "OAuth" }
|
option { value: "oauth", "OAuth" }
|
||||||
option { value: "secret_detection", "Secrets" }
|
option { value: "secret_detection", "Secrets" }
|
||||||
option { value: "lint", "Lint" }
|
option { value: "lint", "Lint" }
|
||||||
option { value: "code_review", "Code Review" }
|
|
||||||
}
|
}
|
||||||
select {
|
select {
|
||||||
onchange: move |e| { status_filter.set(e.value()); page.set(1); },
|
onchange: move |e| { status_filter.set(e.value()); page.set(1); },
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use dioxus_free_icons::Icon;
|
|||||||
use crate::components::page_header::PageHeader;
|
use crate::components::page_header::PageHeader;
|
||||||
use crate::components::toast::{ToastType, Toasts};
|
use crate::components::toast::{ToastType, Toasts};
|
||||||
use crate::infrastructure::mcp::{
|
use crate::infrastructure::mcp::{
|
||||||
add_mcp_server, delete_mcp_server, fetch_mcp_servers, regenerate_mcp_token,
|
add_mcp_server, delete_mcp_server, fetch_mcp_servers, refresh_mcp_status, regenerate_mcp_token,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
@@ -22,6 +22,17 @@ pub fn McpServersPage() -> Element {
|
|||||||
let mut new_mongo_uri = use_signal(String::new);
|
let mut new_mongo_uri = use_signal(String::new);
|
||||||
let mut new_mongo_db = use_signal(String::new);
|
let mut new_mongo_db = use_signal(String::new);
|
||||||
|
|
||||||
|
// Probe health of all MCP servers on page load, then refresh the list
|
||||||
|
let mut refreshing = use_signal(|| true);
|
||||||
|
use_effect(move || {
|
||||||
|
spawn(async move {
|
||||||
|
refreshing.set(true);
|
||||||
|
let _ = refresh_mcp_status().await;
|
||||||
|
servers.restart();
|
||||||
|
refreshing.set(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Track which server's token is visible
|
// Track which server's token is visible
|
||||||
let mut visible_token: Signal<Option<String>> = use_signal(|| None);
|
let mut visible_token: Signal<Option<String>> = use_signal(|| None);
|
||||||
// Track which server is pending delete confirmation
|
// Track which server is pending delete confirmation
|
||||||
|
|||||||
@@ -41,7 +41,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
StreamableHttpServerConfig::default(),
|
StreamableHttpServerConfig::default(),
|
||||||
);
|
);
|
||||||
|
|
||||||
let router = axum::Router::new().nest_service("/mcp", service);
|
let router = axum::Router::new()
|
||||||
|
.route("/health", axum::routing::get(|| async { "ok" }))
|
||||||
|
.nest_service("/mcp", service);
|
||||||
let listener = tokio::net::TcpListener::bind(("0.0.0.0", port)).await?;
|
let listener = tokio::net::TcpListener::bind(("0.0.0.0", port)).await?;
|
||||||
tracing::info!("MCP HTTP server listening on 0.0.0.0:{port}");
|
tracing::info!("MCP HTTP server listening on 0.0.0.0:{port}");
|
||||||
axum::serve(listener, router).await?;
|
axum::serve(listener, router).await?;
|
||||||
|
|||||||
@@ -8,14 +8,14 @@ services:
|
|||||||
container_name: mailserver
|
container_name: mailserver
|
||||||
ports:
|
ports:
|
||||||
- "25:25" # SMTP (inbound mail)
|
- "25:25" # SMTP (inbound mail)
|
||||||
- "143:143" # IMAP (orchestrator reads mail)
|
- "993:993" # IMAPS (TLS-only)
|
||||||
- "993:993" # IMAPS (TLS)
|
- "587:587" # Submission (STARTTLS)
|
||||||
- "587:587" # Submission (outbound, if needed)
|
|
||||||
volumes:
|
volumes:
|
||||||
- maildata:/var/mail
|
- maildata:/var/mail
|
||||||
- mailstate:/var/mail-state
|
- mailstate:/var/mail-state
|
||||||
- maillogs:/var/log/mail
|
- maillogs:/var/log/mail
|
||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
|
- /etc/letsencrypt:/etc/letsencrypt:ro
|
||||||
environment:
|
environment:
|
||||||
# Hostname
|
# Hostname
|
||||||
- OVERRIDE_HOSTNAME=mail.scanner.meghsakha.com
|
- OVERRIDE_HOSTNAME=mail.scanner.meghsakha.com
|
||||||
@@ -34,8 +34,14 @@ services:
|
|||||||
# Plus-addressing (critical for pentest)
|
# Plus-addressing (critical for pentest)
|
||||||
- POSTFIX_RECIPIENT_DELIMITER=+
|
- POSTFIX_RECIPIENT_DELIMITER=+
|
||||||
|
|
||||||
# SSL (start with no TLS, add Let's Encrypt later)
|
# TLS — use Let's Encrypt certs mounted from Coolify/Caddy
|
||||||
- SSL_TYPE=
|
- SSL_TYPE=manual
|
||||||
|
- SSL_CERT_PATH=/etc/letsencrypt/live/mail.scanner.meghsakha.com/fullchain.pem
|
||||||
|
- SSL_KEY_PATH=/etc/letsencrypt/live/mail.scanner.meghsakha.com/privkey.pem
|
||||||
|
|
||||||
|
# Require TLS before accepting PLAIN/LOGIN auth (CERT-Bund compliance)
|
||||||
|
# Disable plaintext auth on unencrypted connections
|
||||||
|
- DOVECOT_DISABLE_PLAINTEXT_AUTH=yes
|
||||||
|
|
||||||
# Accept mail for our domain
|
# Accept mail for our domain
|
||||||
- PERMIT_DOCKER=none
|
- PERMIT_DOCKER=none
|
||||||
|
|||||||
Reference in New Issue
Block a user