Skip to content

Conversation

@mrgeetv
Copy link

@mrgeetv mrgeetv commented Aug 28, 2025

Summary

This PR adds mutual TLS (mTLS) client certificate authentication support to Unpackerr, enabling secure communication with Starr applications (Sonarr, Radarr, Lidarr, Readarr, Whisparr) that are protected behind reverse proxies requiring client certificates.

Testing

  • Tested with real mTLS-protected Sonarr and Radarr instances
  • Verified successful extraction and queue monitoring
  • Confirmed backward compatibility with non-mTLS instances
  • Validated error handling with invalid certificates

Configuration Example

TOML Configuration

[[sonarr]]
url = "https://sonarr.example.com"
api_key = "your-api-key"
tls_client_cert = "/path/to/client.crt"
tls_client_key = "/path/to/client.key"
tls_ca_cert = "/path/to/ca.crt"  # Optional

Docker Compose

volumes:
  - /path/to/certs:/certs:ro
environment:
  - UN_SONARR_0_TLS_CLIENT_CERT=/certs/client.crt
  - UN_SONARR_0_TLS_CLIENT_KEY=/certs/client.key
  - UN_SONARR_0_TLS_CA_CERT=/certs/ca.crt

Implementation Details

The implementation modifies the HTTP transport configuration to include TLS client certificates when provided:

  1. If tls_client_cert and tls_client_key are configured, they are loaded as an X.509 key pair
  2. If tls_ca_cert is provided, it's added to the root CA pool for server certificate verification
  3. The existing valid_ssl flag continues to control certificate validation

- Add TLSClientCert, TLSClientKey, and TLSCACert fields to StarrConfig
- Implement certificate loading and validation in validateApp()
- Support custom CA certificates for private certificate authorities
- Maintain backward compatibility when mTLS fields are not configured
- Add docker volume mount note for certificate directories
- Update all example configurations with mTLS options
- Add static error ErrInvalidCA for wrapped error handling (err113)
- Extract configureTLS helper function to reduce complexity (funlen)
- Move embedded field to top of struct declaration
- Align all struct tags in columns (tagalign)
- Add nolint comment for unavoidable line length
app, conf.URL, expandHomedir(conf.TLSCACert), err)
}

caCertPool := x509.NewCertPool()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
caCertPool := x509.NewCertPool()
tlsConfig.RootCA = x509.NewCertPool()

Can you just set this directly here? (I'm not sure if .RootCAs is an interface or not.)

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

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extra whitespace you don't need here.

Comment on lines +42 to 45
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"`
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you consider this instead? You'll have to make a few other changes, but I think this will be a bit cleaner in the end.

Suggested change
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"`
}
TLS *ClientTLS `json:"tls" toml:"tls" xml:"tls" yaml:"tls"`
}
type ClientTLS struct {
Cert string `json:"client_cert" toml:"client_cert" xml:"client_cert" yaml:"client_cert"`
Key string `json:"client_key" toml:"client_key" xml:"client_key" yaml:"client_key"`
CA string `json:"ca_cert" toml:"ca_cert" xml:"ca_cert" yaml:"ca_cert"`
}

tlsConfig := &tls.Config{InsecureSkipVerify: !conf.ValidSSL} //nolint:gosec

// Add mTLS if certificates are configured
if conf.TLSClientCert != "" && conf.TLSClientKey != "" {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if conf.TLSClientCert != "" && conf.TLSClientKey != "" {
if conf.TLSClientCert != "" || conf.TLSClientKey != "" {

I usually do an "or" on these two variables when dealing with SSL. In case a user sets one, they'll get an error. Instead of nothing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants