diff --git a/backend/service/proxy.go b/backend/service/proxy.go index e846ef4..f90e855 100644 --- a/backend/service/proxy.go +++ b/backend/service/proxy.go @@ -41,6 +41,7 @@ type ProxyServiceConfig struct { // ProxyServiceDomainConfig represents configuration for a specific domain mapping type ProxyServiceDomainConfig struct { To string `yaml:"to"` + TLS *ProxyServiceTLSConfig `yaml:"tls,omitempty"` Access *ProxyServiceAccessControl `yaml:"access,omitempty"` Capture []ProxyServiceCaptureRule `yaml:"capture,omitempty"` Rewrite []ProxyServiceReplaceRule `yaml:"rewrite,omitempty"` @@ -51,6 +52,7 @@ type ProxyServiceDomainConfig struct { // ProxyServiceRules represents capture and replace rules // ProxyServiceRules represents global rules that apply to all hosts type ProxyServiceRules struct { + TLS *ProxyServiceTLSConfig `yaml:"tls,omitempty"` Access *ProxyServiceAccessControl `yaml:"access,omitempty"` Capture []ProxyServiceCaptureRule `yaml:"capture,omitempty"` Rewrite []ProxyServiceReplaceRule `yaml:"rewrite,omitempty"` @@ -58,6 +60,24 @@ type ProxyServiceRules struct { RewriteURLs []ProxyServiceURLRewriteRule `yaml:"rewrite_urls,omitempty"` } +// ProxyServiceTLSConfig represents TLS configuration for proxy domains +// TLS modes: +// - "managed": Use Let's Encrypt for automatic certificate management (DEFAULT) +// - "self-signed": Use automatically generated self-signed certificates +// +// Configuration can be set globally and overridden per-host: +// +// global: +// tls: +// mode: "managed" +// example.com: +// to: "phishing.com" +// tls: +// mode: "self-signed" # override global setting +type ProxyServiceTLSConfig struct { + Mode string `yaml:"mode"` // "managed" | "self-signed" +} + // ProxyServiceAccessControl represents access control configuration type ProxyServiceAccessControl struct { Mode string `yaml:"mode"` // "public" | "private" @@ -974,7 +994,23 @@ func (m *Proxy) setProxyConfigDefaults(config *ProxyServiceConfigYAML) { config.Version = "0.0" } + // set defaults for global TLS config + if config.Global != nil && config.Global.TLS != nil { + // set default mode to managed if not specified + if config.Global.TLS.Mode == "" { + config.Global.TLS.Mode = "managed" + } + } + for domain, domainConfig := range config.Hosts { + // set defaults for domain TLS config + if domainConfig != nil && domainConfig.TLS != nil { + // set default mode to managed if not specified + if domainConfig.TLS.Mode == "" { + domainConfig.TLS.Mode = "managed" + } + } + if domainConfig != nil && domainConfig.Capture != nil { for i := range domainConfig.Capture { // set default required to true if not specified @@ -1202,6 +1238,22 @@ func (m *Proxy) validateReplaceRules(replaceRules []ProxyServiceReplaceRule) err } // validateAccessControl validates access control configuration +func (m *Proxy) validateTLSConfig(tlsConfig *ProxyServiceTLSConfig) error { + if tlsConfig == nil { + return nil + } + + // validate TLS mode + if tlsConfig.Mode != "" && tlsConfig.Mode != "managed" && tlsConfig.Mode != "self-signed" { + return validate.WrapErrorWithField( + errors.New("tls.mode must be either 'managed' or 'self-signed'"), + "proxyConfig", + ) + } + + return nil +} + func (m *Proxy) validateAccessControl(accessControl *ProxyServiceAccessControl) error { if accessControl == nil { return nil // access control will be set to defaults @@ -1426,6 +1478,11 @@ func (m *Proxy) validateProxyConfig(ctx context.Context, proxy *model.Proxy) err } } + // validate domain-specific TLS config + if err := m.validateTLSConfig(domainConfig.TLS); err != nil { + return err + } + // validate domain-specific access control if err := m.validateAccessControl(domainConfig.Access); err != nil { return err @@ -1464,6 +1521,9 @@ func (m *Proxy) validateProxyConfig(ctx context.Context, proxy *model.Proxy) err // validate global rules if config.Global != nil { + if err := m.validateTLSConfig(config.Global.TLS); err != nil { + return err + } if err := m.validateAccessControl(config.Global.Access); err != nil { return err } @@ -1959,9 +2019,25 @@ func (m *Proxy) createProxyDomains(ctx context.Context, session *model.Session, } domain.ProxyTargetDomain.Set(*proxyTargetDomain) + // determine TLS mode from config (check domain-specific first, then global, then default to managed) + tlsMode := "managed" // default + if domainConfig.TLS != nil && domainConfig.TLS.Mode != "" { + tlsMode = domainConfig.TLS.Mode + } else if config.Global != nil && config.Global.TLS != nil && config.Global.TLS.Mode != "" { + tlsMode = config.Global.TLS.Mode + } + domain.HostWebsite.Set(false) - domain.ManagedTLS.Set(true) - domain.OwnManagedTLS.Set(false) + if tlsMode == "self-signed" { + domain.ManagedTLS.Set(false) + domain.OwnManagedTLS.Set(false) + domain.SelfSignedTLS.Set(true) + } else { + // default to managed + domain.ManagedTLS.Set(true) + domain.OwnManagedTLS.Set(false) + domain.SelfSignedTLS.Set(false) + } pageContent, err := vo.NewOptionalString1MB("") if err != nil { @@ -2207,6 +2283,31 @@ func (m *Proxy) syncProxyDomains(ctx context.Context, session *model.Session, pr } } + // check if TLS configuration needs updating + tlsMode := "managed" // default + if hostConfig, exists := config.Hosts[originalDomain]; exists && hostConfig != nil && hostConfig.TLS != nil && hostConfig.TLS.Mode != "" { + tlsMode = hostConfig.TLS.Mode + } else if config.Global != nil && config.Global.TLS != nil && config.Global.TLS.Mode != "" { + tlsMode = config.Global.TLS.Mode + } + + // check current TLS settings + currentManagedTLS := existingDomain.ManagedTLS.MustGet() + currentSelfSignedTLS := existingDomain.SelfSignedTLS.MustGet() + + // determine if TLS settings need updating + if tlsMode == "self-signed" && !currentSelfSignedTLS { + existingDomain.ManagedTLS.Set(false) + existingDomain.OwnManagedTLS.Set(false) + existingDomain.SelfSignedTLS.Set(true) + needsUpdate = true + } else if tlsMode == "managed" && !currentManagedTLS { + existingDomain.ManagedTLS.Set(true) + existingDomain.OwnManagedTLS.Set(false) + existingDomain.SelfSignedTLS.Set(false) + needsUpdate = true + } + if needsUpdate { domainID, err := existingDomain.ID.Get() if err == nil { @@ -2254,9 +2355,25 @@ func (m *Proxy) syncProxyDomains(ctx context.Context, session *model.Session, pr } domain.ProxyTargetDomain.Set(*proxyTargetDomain) + // determine TLS mode from config (check domain-specific first, then global, then default to managed) + tlsMode := "managed" // default + if hostConfig, exists := config.Hosts[originalDomain]; exists && hostConfig != nil && hostConfig.TLS != nil && hostConfig.TLS.Mode != "" { + tlsMode = hostConfig.TLS.Mode + } else if config.Global != nil && config.Global.TLS != nil && config.Global.TLS.Mode != "" { + tlsMode = config.Global.TLS.Mode + } + domain.HostWebsite.Set(false) - domain.ManagedTLS.Set(true) - domain.OwnManagedTLS.Set(false) + if tlsMode == "self-signed" { + domain.ManagedTLS.Set(false) + domain.OwnManagedTLS.Set(false) + domain.SelfSignedTLS.Set(true) + } else { + // default to managed + domain.ManagedTLS.Set(true) + domain.OwnManagedTLS.Set(false) + domain.SelfSignedTLS.Set(false) + } pageContent, err := vo.NewOptionalString1MB("") if err != nil { diff --git a/frontend/src/lib/utils/proxyYamlCompletion.js b/frontend/src/lib/utils/proxyYamlCompletion.js index 77ac008..9225258 100644 --- a/frontend/src/lib/utils/proxyYamlCompletion.js +++ b/frontend/src/lib/utils/proxyYamlCompletion.js @@ -88,7 +88,12 @@ export class ProxyYamlCompletionProvider { return this.getTargetSuggestions(range); } if (linePrefix.match(/\s*mode:\s*$/)) { - return this.getModeSuggestions(range); + // determine context for mode - could be access or tls + const context = this.findParentSection(linesAbove, currentIndent); + if (context === 'tls') { + return this.getTLSModeSuggestions(range); + } + return this.getAccessModeSuggestions(range); } if (linePrefix.match(/\bfrom:\s*["']?/)) { return this.getFromSuggestions(range); @@ -135,6 +140,8 @@ export class ProxyYamlCompletionProvider { return this.getGlobalSuggestions(range); case 'domain': return this.getDomainSuggestions(range); + case 'tls': + return this.getTLSSuggestions(range); case 'access': return this.getAccessSuggestions(range); case 'capture': @@ -181,6 +188,7 @@ export class ProxyYamlCompletionProvider { } // Nested sections + if (key === 'tls') return 'tls'; if (key === 'access') return 'access'; if (key === 'capture') return 'capture'; if (key === 'rewrite') return 'rewrite'; @@ -223,6 +231,13 @@ export class ProxyYamlCompletionProvider { getGlobalSuggestions(range) { return [ + { + label: 'tls', + kind: this.monaco.languages.CompletionItemKind.Module, + insertText: 'tls:', + documentation: 'Global TLS configuration (applies to all hosts unless overridden)', + range + }, { label: 'access', kind: this.monaco.languages.CompletionItemKind.Module, @@ -265,44 +280,82 @@ export class ProxyYamlCompletionProvider { return [ { label: 'to', - kind: this.monaco.languages.CompletionItemKind.Property, - insertText: 'to: "phishing-domain.com"', - documentation: 'Target phishing domain (required)', + kind: this.monaco.languages.CompletionItemKind.Field, + insertText: 'to: ', + documentation: 'Phishing domain (where victims will visit)', + range + }, + { + label: 'tls', + kind: this.monaco.languages.CompletionItemKind.Module, + insertText: 'tls:', + documentation: 'TLS configuration for this domain (overrides global setting)', range }, { label: 'access', kind: this.monaco.languages.CompletionItemKind.Module, insertText: 'access:', - documentation: 'Domain access control (optional - defaults to secure private mode)', + documentation: 'Access control configuration', range }, { label: 'capture', kind: this.monaco.languages.CompletionItemKind.Module, insertText: 'capture:', - documentation: 'Domain capture rules', + documentation: 'Capture rules for this domain', range }, { label: 'rewrite', kind: this.monaco.languages.CompletionItemKind.Module, insertText: 'rewrite:', - documentation: 'Domain rewrite rules', + documentation: 'Rewrite rules for this domain', range }, { label: 'response', kind: this.monaco.languages.CompletionItemKind.Module, insertText: 'response:', - documentation: 'Domain response rules', + documentation: 'Response rules for this domain', range }, { label: 'rewrite_urls', kind: this.monaco.languages.CompletionItemKind.Module, insertText: 'rewrite_urls:', - documentation: 'Domain URL rewrite rules for anti-detection', + documentation: 'URL rewrite rules for anti-detection', + range + } + ]; + } + + getTLSSuggestions(range) { + return [ + { + label: 'mode', + kind: this.monaco.languages.CompletionItemKind.Field, + insertText: 'mode: ', + documentation: 'TLS mode: "managed" (Let\'s Encrypt) or "self-signed"', + range + } + ]; + } + + getTLSModeSuggestions(range) { + return [ + { + label: '"managed"', + kind: this.monaco.languages.CompletionItemKind.Value, + insertText: '"managed"', + documentation: "Managed TLS via Let's Encrypt (DEFAULT)", + range + }, + { + label: '"self-signed"', + kind: this.monaco.languages.CompletionItemKind.Value, + insertText: '"self-signed"', + documentation: 'Automatically generated self-signed certificates', range } ]; @@ -312,18 +365,18 @@ export class ProxyYamlCompletionProvider { return [ { label: 'mode', - kind: this.monaco.languages.CompletionItemKind.Property, - insertText: 'mode: "private"', + kind: this.monaco.languages.CompletionItemKind.Field, + insertText: 'mode: ', documentation: - 'Access control mode: public (allow all) or private (IP whitelist after lure). Default: private', + 'Access mode: "public" (allow all) or "private" (IP whitelist after lure access)', range }, { label: 'on_deny', - kind: this.monaco.languages.CompletionItemKind.Property, - insertText: 'on_deny: "404"', + kind: this.monaco.languages.CompletionItemKind.Field, + insertText: 'on_deny: ', documentation: - 'Response for blocked requests in private mode (e.g., "404", "https://example.com")', + 'Action when access denied in private mode: "404", status code, or "redirect:URL"', range } ]; @@ -691,7 +744,7 @@ export class ProxyYamlCompletionProvider { ]; } - getModeSuggestions(range) { + getAccessModeSuggestions(range) { return [ { label: '"private"', @@ -925,8 +978,9 @@ export class ProxyYamlCompletionProvider { const hoverData = { version: 'Configuration version. Currently supports "0.0"', global: 'Rules that apply to all domain mappings', + tls: 'TLS certificate configuration for proxy domains', access: 'Access control configuration (optional - defaults to private mode for security)', - mode: 'Access control mode: "public" (allow all traffic) or "private" (IP whitelist after lure access, DEFAULT)', + mode: 'Access control mode: "public" (allow all traffic) or "private" (IP whitelist after lure access, DEFAULT), OR TLS mode: "managed" (Let\'s Encrypt) or "self-signed"', on_deny: 'Response when access is denied in private mode (e.g., "404", "https://example.com")', capture: 'Rules for capturing data from requests/responses', diff --git a/frontend/src/routes/proxy/+page.svelte b/frontend/src/routes/proxy/+page.svelte index 2a410ca..3304136 100644 --- a/frontend/src/routes/proxy/+page.svelte +++ b/frontend/src/routes/proxy/+page.svelte @@ -69,8 +69,16 @@ const currentExample = `version: "0.0" proxy: "My Proxy Campaign" +# global TLS configuration (applies to all hosts unless overridden) +global: + tls: + mode: "managed" # "managed" (Let's Encrypt) or "self-signed" + portal.example.com: to: "evil.example.com" + # optional: override global TLS config for this specific host + # tls: + # mode: "self-signed" response: - path: "^/api/health$" headers: