diff --git a/examples/docker-compose.yml b/examples/docker-compose.yml index 3c02a89..21fca49 100644 --- a/examples/docker-compose.yml +++ b/examples/docker-compose.yml @@ -12,6 +12,8 @@ services: # You need at least this one volume mapped so Unpackerr can find your files to extract. # Make sure this matches your Starr apps; the folder mount (/downloads or /data) should be identical. - /mnt/HostDownloads:/downloads + # OPTIONAL: If using mTLS certificates, mount the certificate directory. + # - /path/to/certs:/certs:ro restart: always # Get the user:group correct so unpackerr can read and write to your files. user: ${PUID}:${PGID} @@ -62,6 +64,9 @@ services: - UN_SONARR_0_DELETE_DELAY=5m - UN_SONARR_0_DELETE_ORIG=false - UN_SONARR_0_SYNCTHING=false + - UN_SONARR_0_TLS_CLIENT_CERT=/path/to/client.crt + - UN_SONARR_0_TLS_CLIENT_KEY=/path/to/client.key + - UN_SONARR_0_TLS_CA_CERT=/path/to/ca-bundle.crt ## Radarr Settings - UN_RADARR_0_URL=http://radarr:7878 - UN_RADARR_0_API_KEY=0123456789abcdef0123456789abcdef @@ -71,6 +76,9 @@ services: - UN_RADARR_0_DELETE_DELAY=5m - UN_RADARR_0_DELETE_ORIG=false - UN_RADARR_0_SYNCTHING=false + - UN_RADARR_0_TLS_CLIENT_CERT=/path/to/client.crt + - UN_RADARR_0_TLS_CLIENT_KEY=/path/to/client.key + - UN_RADARR_0_TLS_CA_CERT=/path/to/ca-bundle.crt ## Lidarr Settings - UN_LIDARR_0_URL=http://lidarr:8686 - UN_LIDARR_0_API_KEY=0123456789abcdef0123456789abcdef @@ -80,6 +88,9 @@ services: - UN_LIDARR_0_DELETE_DELAY=5m - UN_LIDARR_0_DELETE_ORIG=false - UN_LIDARR_0_SYNCTHING=false + - UN_LIDARR_0_TLS_CLIENT_CERT=/path/to/client.crt + - UN_LIDARR_0_TLS_CLIENT_KEY=/path/to/client.key + - UN_LIDARR_0_TLS_CA_CERT=/path/to/ca-bundle.crt ## Readarr Settings - UN_READARR_0_URL=http://readarr:8787 - UN_READARR_0_API_KEY=0123456789abcdef0123456789abcdef @@ -89,6 +100,9 @@ services: - UN_READARR_0_DELETE_DELAY=5m - UN_READARR_0_DELETE_ORIG=false - UN_READARR_0_SYNCTHING=false + - UN_READARR_0_TLS_CLIENT_CERT=/path/to/client.crt + - UN_READARR_0_TLS_CLIENT_KEY=/path/to/client.key + - UN_READARR_0_TLS_CA_CERT=/path/to/ca-bundle.crt ## Whisparr Settings - UN_WHISPARR_0_URL=http://whisparr:6969 - UN_WHISPARR_0_API_KEY=0123456789abcdef0123456789abcdef @@ -98,6 +112,9 @@ services: - UN_WHISPARR_0_DELETE_DELAY=5m - UN_WHISPARR_0_DELETE_ORIG=false - UN_WHISPARR_0_SYNCTHING=false + - UN_WHISPARR_0_TLS_CLIENT_CERT=/path/to/client.crt + - UN_WHISPARR_0_TLS_CLIENT_KEY=/path/to/client.key + - UN_WHISPARR_0_TLS_CA_CERT=/path/to/ca-bundle.crt ## Watch Folders - UN_FOLDER_0_PATH=/downloads/auto_extract - UN_FOLDER_0_EXTRACT_PATH= @@ -136,4 +153,4 @@ services: - UN_CMDHOOK_0_EXCLUDE_1=lidarr - UN_CMDHOOK_0_TIMEOUT=10s -## => Content Auto Generated, 12 APR 2025 04:54 UTC +## => Content Auto Generated, 28 AUG 2025 20:00 UTC diff --git a/examples/unpackerr.conf.example b/examples/unpackerr.conf.example index f3b0bff..cecf488 100644 --- a/examples/unpackerr.conf.example +++ b/examples/unpackerr.conf.example @@ -132,6 +132,18 @@ dir_mode = "0755" # delete_orig = false ## If you use Syncthing, setting this to true will make unpackerr wait for syncs to finish. # syncthing = false +## Path to TLS client certificate file for mutual TLS authentication. +## Required when your Starr app is behind a reverse proxy with mTLS enabled. +## Must be paired with tls_client_key. Leave empty for standard TLS connections. +# tls_client_cert = "/path/to/client.crt" +## Path to TLS client private key file for mutual TLS authentication. +## This key must match the tls_client_cert certificate. +## Both cert and key are required for mTLS to work. +# tls_client_key = "/path/to/client.key" +## Path to custom Certificate Authority bundle to verify server certificate. +## Use this when your Starr app uses a certificate signed by a private CA. +## If not set, system default CA bundle is used. +# tls_ca_cert = "/path/to/ca-bundle.crt" ## Leaving the [[radarr]] header uncommented (no leading hash #) without also ## uncommenting the api_key (remove the hash #) will produce a startup warning. @@ -153,6 +165,18 @@ dir_mode = "0755" # delete_orig = false ## If you use Syncthing, setting this to true will make unpackerr wait for syncs to finish. # syncthing = false +## Path to TLS client certificate file for mutual TLS authentication. +## Required when your Starr app is behind a reverse proxy with mTLS enabled. +## Must be paired with tls_client_key. Leave empty for standard TLS connections. +# tls_client_cert = "/path/to/client.crt" +## Path to TLS client private key file for mutual TLS authentication. +## This key must match the tls_client_cert certificate. +## Both cert and key are required for mTLS to work. +# tls_client_key = "/path/to/client.key" +## Path to custom Certificate Authority bundle to verify server certificate. +## Use this when your Starr app uses a certificate signed by a private CA. +## If not set, system default CA bundle is used. +# tls_ca_cert = "/path/to/ca-bundle.crt" #[[lidarr]] # url = "http://127.0.0.1:8686" @@ -172,6 +196,18 @@ dir_mode = "0755" # delete_orig = false ## If you use Syncthing, setting this to true will make unpackerr wait for syncs to finish. # syncthing = false +## Path to TLS client certificate file for mutual TLS authentication. +## Required when your Starr app is behind a reverse proxy with mTLS enabled. +## Must be paired with tls_client_key. Leave empty for standard TLS connections. +# tls_client_cert = "/path/to/client.crt" +## Path to TLS client private key file for mutual TLS authentication. +## This key must match the tls_client_cert certificate. +## Both cert and key are required for mTLS to work. +# tls_client_key = "/path/to/client.key" +## Path to custom Certificate Authority bundle to verify server certificate. +## Use this when your Starr app uses a certificate signed by a private CA. +## If not set, system default CA bundle is used. +# tls_ca_cert = "/path/to/ca-bundle.crt" #[[readarr]] # url = "http://127.0.0.1:8787" @@ -191,6 +227,18 @@ dir_mode = "0755" # delete_orig = false ## If you use Syncthing, setting this to true will make unpackerr wait for syncs to finish. # syncthing = false +## Path to TLS client certificate file for mutual TLS authentication. +## Required when your Starr app is behind a reverse proxy with mTLS enabled. +## Must be paired with tls_client_key. Leave empty for standard TLS connections. +# tls_client_cert = "/path/to/client.crt" +## Path to TLS client private key file for mutual TLS authentication. +## This key must match the tls_client_cert certificate. +## Both cert and key are required for mTLS to work. +# tls_client_key = "/path/to/client.key" +## Path to custom Certificate Authority bundle to verify server certificate. +## Use this when your Starr app uses a certificate signed by a private CA. +## If not set, system default CA bundle is used. +# tls_ca_cert = "/path/to/ca-bundle.crt" #[[whisparr]] # url = "http://127.0.0.1:6969" @@ -210,6 +258,18 @@ dir_mode = "0755" # delete_orig = false ## If you use Syncthing, setting this to true will make unpackerr wait for syncs to finish. # syncthing = false +## Path to TLS client certificate file for mutual TLS authentication. +## Required when your Starr app is behind a reverse proxy with mTLS enabled. +## Must be paired with tls_client_key. Leave empty for standard TLS connections. +# tls_client_cert = "/path/to/client.crt" +## Path to TLS client private key file for mutual TLS authentication. +## This key must match the tls_client_cert certificate. +## Both cert and key are required for mTLS to work. +# tls_client_key = "/path/to/client.key" +## Path to custom Certificate Authority bundle to verify server certificate. +## Use this when your Starr app uses a certificate signed by a private CA. +## If not set, system default CA bundle is used. +# tls_ca_cert = "/path/to/ca-bundle.crt" ################################################################################## ### ### STOP HERE ### STOP HERE ### STOP HERE ### STOP HERE #### STOP HERE ### # @@ -302,4 +362,4 @@ dir_mode = "0755" ## You can adjust how long to wait for the command to run. # timeout = "10s" -## => Content Auto Generated, 12 APR 2025 04:54 UTC +## => Content Auto Generated, 28 AUG 2025 20:00 UTC diff --git a/init/config/compose.go b/init/config/compose.go index 3c984ad..4fd946d 100644 --- a/init/config/compose.go +++ b/init/config/compose.go @@ -25,6 +25,8 @@ services: # You need at least this one volume mapped so Unpackerr can find your files to extract. # Make sure this matches your Starr apps; the folder mount (/downloads or /data) should be identical. - /mnt/HostDownloads:/downloads + # OPTIONAL: If using mTLS certificates, mount the certificate directory. + # - /path/to/certs:/certs:ro restart: always # Get the user:group correct so unpackerr can read and write to your files. user: ${PUID}:${PGID} diff --git a/init/config/definitions.yml b/init/config/definitions.yml index 6b84589..5ed8e1f 100644 --- a/init/config/definitions.yml +++ b/init/config/definitions.yml @@ -493,6 +493,33 @@ sections: recommend: *BOOLEAN short: Setting this to true makes unpackerr wait for syncthing to finish. desc: If you use Syncthing, setting this to true will make unpackerr wait for syncs to finish. + - name: tls_client_cert + envvar: TLS_CLIENT_CERT + default: '' + example: /path/to/client.crt + short: Path to TLS client certificate for mTLS. + desc: | + Path to TLS client certificate file for mutual TLS authentication. + Required when your Starr app is behind a reverse proxy with mTLS enabled. + Must be paired with tls_client_key. Leave empty for standard TLS connections. + - name: tls_client_key + envvar: TLS_CLIENT_KEY + default: '' + example: /path/to/client.key + short: Path to TLS client private key for mTLS. + desc: | + Path to TLS client private key file for mutual TLS authentication. + This key must match the tls_client_cert certificate. + Both cert and key are required for mTLS to work. + - name: tls_ca_cert + envvar: TLS_CA_CERT + default: '' + example: /path/to/ca-bundle.crt + short: Path to custom CA certificate. + desc: | + Path to custom Certificate Authority bundle to verify server certificate. + Use this when your Starr app uses a certificate signed by a private CA. + If not set, system default CA bundle is used. # Global folder configuration. folders: diff --git a/pkg/unpackerr/apps.go b/pkg/unpackerr/apps.go index 575bb66..649045d 100644 --- a/pkg/unpackerr/apps.go +++ b/pkg/unpackerr/apps.go @@ -36,6 +36,7 @@ const ( var ( ErrInvalidURL = errors.New("provided application URL is invalid") ErrInvalidKey = fmt.Errorf("provided application API Key is invalid, must be %d characters", apiKeyLength) + ErrInvalidCA = errors.New("failed parsing CA certificate") ) // Config defines the configuration data used to start the application. diff --git a/pkg/unpackerr/cnfgfile.go b/pkg/unpackerr/cnfgfile.go index fa6b7c7..3d637f9 100644 --- a/pkg/unpackerr/cnfgfile.go +++ b/pkg/unpackerr/cnfgfile.go @@ -2,6 +2,7 @@ package unpackerr import ( "crypto/tls" + "crypto/x509" "fmt" "net/http" "os" @@ -311,12 +312,60 @@ func (u *Unpackerr) validateApp(conf *StarrConfig, app starr.App) error { conf.Protocols = defaultProtocol } + // Configure TLS and HTTP client + tlsConfig, err := u.configureTLS(conf, app) + if err != nil { + return err + } + conf.Config.Client = &http.Client{ Timeout: conf.Timeout.Duration, Transport: &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: !conf.ValidSSL}, //nolint:gosec + TLSClientConfig: tlsConfig, }, } return nil } + +// configureTLS creates and configures the TLS config for mTLS support. +func (u *Unpackerr) configureTLS(conf *StarrConfig, app starr.App) (*tls.Config, error) { + // Create TLS config - default behavior unchanged + tlsConfig := &tls.Config{InsecureSkipVerify: !conf.ValidSSL} //nolint:gosec + + // Add mTLS if certificates are configured + if conf.TLSClientCert != "" && conf.TLSClientKey != "" { + certPath := expandHomedir(conf.TLSClientCert) + keyPath := expandHomedir(conf.TLSClientKey) + + cert, err := tls.LoadX509KeyPair(certPath, keyPath) + if err != nil { + return nil, fmt.Errorf("%s (%s) failed loading TLS client cert from %s and %s: %w", + app, conf.URL, certPath, keyPath, err) + } + + tlsConfig.Certificates = []tls.Certificate{cert} + + u.Debugf("%s (%s): Loaded mTLS client certificate", app, conf.URL) + } + + // Add custom CA if configured + if conf.TLSCACert != "" { + caCert, err := os.ReadFile(expandHomedir(conf.TLSCACert)) + if err != nil { + return nil, fmt.Errorf("%s (%s) failed reading CA cert from %s: %w", + app, conf.URL, expandHomedir(conf.TLSCACert), err) + } + + caCertPool := x509.NewCertPool() + if !caCertPool.AppendCertsFromPEM(caCert) { + return nil, fmt.Errorf("%w: %s (%s) from %s", ErrInvalidCA, app, conf.URL, expandHomedir(conf.TLSCACert)) + } + + tlsConfig.RootCAs = caCertPool + + u.Debugf("%s (%s): Loaded custom CA certificate", app, conf.URL) + } + + return tlsConfig, nil +} diff --git a/pkg/unpackerr/handlers.go b/pkg/unpackerr/handlers.go index cf01ef4..eac4407 100644 --- a/pkg/unpackerr/handlers.go +++ b/pkg/unpackerr/handlers.go @@ -29,15 +29,19 @@ type Extract struct { // Shared config items for all starr apps. type StarrConfig struct { - Path string `json:"path" toml:"path" xml:"path" yaml:"path"` - Paths StringSlice `json:"paths" toml:"paths" xml:"paths" yaml:"paths"` - Protocols string `json:"protocols" toml:"protocols" xml:"protocols" yaml:"protocols"` - DeleteOrig bool `json:"delete_orig" toml:"delete_orig" xml:"delete_orig" yaml:"delete_orig"` - DeleteDelay cnfg.Duration `json:"delete_delay" toml:"delete_delay" xml:"delete_delay" yaml:"delete_delay"` - Syncthing bool `json:"syncthing" toml:"syncthing" xml:"syncthing" yaml:"syncthing"` - ValidSSL bool `json:"valid_ssl" toml:"valid_ssl" xml:"valid_ssl" yaml:"valid_ssl"` - Timeout cnfg.Duration `json:"timeout" toml:"timeout" xml:"timeout" yaml:"timeout"` starr.Config + + Path string `json:"path" toml:"path" xml:"path" yaml:"path"` + Paths StringSlice `json:"paths" toml:"paths" xml:"paths" yaml:"paths"` + Protocols string `json:"protocols" toml:"protocols" xml:"protocols" yaml:"protocols"` + DeleteOrig bool `json:"delete_orig" toml:"delete_orig" xml:"delete_orig" yaml:"delete_orig"` + DeleteDelay cnfg.Duration `json:"delete_delay" toml:"delete_delay" xml:"delete_delay" yaml:"delete_delay"` + Syncthing bool `json:"syncthing" toml:"syncthing" xml:"syncthing" yaml:"syncthing"` + ValidSSL bool `json:"valid_ssl" toml:"valid_ssl" xml:"valid_ssl" yaml:"valid_ssl"` + Timeout cnfg.Duration `json:"timeout" toml:"timeout" xml:"timeout" yaml:"timeout"` + TLSClientCert string `json:"tls_client_cert" toml:"tls_client_cert" xml:"tls_client_cert" yaml:"tls_client_cert"` //nolint:lll + TLSClientKey string `json:"tls_client_key" toml:"tls_client_key" xml:"tls_client_key" yaml:"tls_client_key"` + TLSCACert string `json:"tls_ca_cert" toml:"tls_ca_cert" xml:"tls_ca_cert" yaml:"tls_ca_cert"` } // checkQueueChanges checks each item for state changes from the app queues.