diff --git a/README.md b/README.md index 26e5ba70..a9b1b4ca 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,79 @@ Memorising docker commands is hard. Memorising aliases is slightly less hard. Ke - Docker >= **1.13** (API >= **1.25**) - Docker-Compose >= **1.23.2** (optional) +## TLS Configuration + +Lazydocker supports secure connections to Docker daemon using TLS. To enable TLS, you can configure it in your config.yml: + +### Basic TLS Configuration + +```yaml +tls: + enable: true + caCertPath: "/path/to/ca.pem" + certPath: "/path/to/cert.pem" + keyPath: "/path/to/key.pem" +``` + +### Complete TLS Configuration + +```yaml +tls: + enable: true # Enable TLS connections to Docker daemon + caCertPath: "/path/to/ca.pem" # Path to Certificate Authority (CA) certificate + certPath: "/path/to/cert.pem" # Path to client certificate + keyPath: "/path/to/key.pem" # Path to client private key + host: "docker.example.com" # Hostname or IP for certificate validation (ServerName) + insecureSkipVerify: false # Skip TLS certificate verification (NOT recommended for production) +``` + +### TLS Configuration Options + +- **`enable`**: Set to `true` to enable TLS connections to the Docker daemon +- **`caCertPath`**: Path to the Certificate Authority (CA) certificate file used to verify the Docker daemon's certificate +- **`certPath`**: Path to the client certificate file for mutual TLS authentication +- **`keyPath`**: Path to the client private key file corresponding to the client certificate +- **`host`**: The hostname or IP address expected on the Docker daemon's certificate. This must match the Common Name (CN) or a Subject Alternative Name (SAN) in the server's certificate +- **`insecureSkipVerify`**: When set to `true`, skips certificate verification (useful for testing, but insecure for production use) + +### Important Notes + +1. **Two Different "Hosts"**: Don't confuse the `tls.host` config field with the `DOCKER_HOST` environment variable: + - `DOCKER_HOST` environment variable: Tells Lazydocker **where to connect** (e.g., `tcp://192.168.1.100:2376`) + - `tls.host` in config.yml: Tells Lazydocker **what name to expect on the certificate** (e.g., `docker.example.com` or `192.168.1.100`) + +2. **Certificate Validation**: The `host` field must match a name in your Docker daemon's certificate: + - Common Name (CN) in the certificate's Subject field + - Or one of the Subject Alternative Names (SANs) in the certificate + +3. **Security**: Always use `insecureSkipVerify: false` in production environments. Only set it to `true` for testing purposes. + +### Example for Remote Docker Daemon + +When connecting to a Docker daemon running on a remote machine with TLS: + +1. Set the connection endpoint: + ```bash + export DOCKER_HOST=tcp://docker.example.com:2376 + ``` + +2. Configure TLS in `config.yml`: + ```yaml + tls: + enable: true + caCertPath: "/home/user/.docker/ca.pem" + certPath: "/home/user/.docker/cert.pem" + keyPath: "/home/user/.docker/key.pem" + host: "docker.example.com" # Must match the name on the server's certificate + insecureSkipVerify: false + ``` + +### Troubleshooting TLS Issues + +- **"x509: certificate is valid for ..., not ..."**: The `host` field doesn't match any name on the server's certificate. Check your server certificate's CN and SANs. +- **"certificate signed by unknown authority"**: The CA certificate path is incorrect or the server's certificate wasn't signed by the specified CA. +- **"no such host"**: This is a network connectivity issue with `DOCKER_HOST`, not a TLS configuration problem. + ## Installation ### Homebrew diff --git a/docs/Config.md b/docs/Config.md index d386ff31..35bfd55d 100644 --- a/docs/Config.md +++ b/docs/Config.md @@ -64,6 +64,13 @@ logs: timestamps: false since: '60m' # set to '' to show all logs tail: '' # set to 200 to show last 200 lines of logs +tls: + enable: false # Set to true to enable TLS connections to Docker daemon + caCertPath: "" # Path to Certificate Authority (CA) certificate file + certPath: "" # Path to client certificate file for mutual TLS authentication + keyPath: "" # Path to client private key file + host: "" # Hostname or IP for certificate validation (must match server certificate) + insecureSkipVerify: false # Skip certificate verification (NOT recommended for production) commandTemplates: dockerCompose: docker compose # Determines the Docker Compose command to run, referred to as .DockerCompose in commandTemplates restartService: '{{ .DockerCompose }} restart {{ .Service.Name }}' @@ -97,6 +104,87 @@ stats: ## To see what all of the config options mean, and what other options you can set, see [here](https://godoc.org/github.com/jesseduffield/lazydocker/pkg/config) +## TLS Configuration + +Lazydocker supports secure connections to Docker daemon using TLS (Transport Layer Security). This is essential when connecting to remote Docker daemons or when your Docker daemon requires client certificate authentication. + +### Configuration Options + +The `tls` section in your config supports the following options: + +- **`enable`**: Boolean flag to enable/disable TLS connections +- **`caCertPath`**: Path to the Certificate Authority (CA) certificate file used to verify the Docker daemon's certificate +- **`certPath`**: Path to the client certificate file for mutual TLS authentication +- **`keyPath`**: Path to the client private key file corresponding to the client certificate +- **`host`**: The hostname or IP address expected on the Docker daemon's certificate. This must match the Common Name (CN) or a Subject Alternative Name (SAN) in the server's certificate +- **`insecureSkipVerify`**: When set to `true`, skips certificate verification. **Only use this for testing - it's insecure for production!** + +### Basic Example + +```yaml +tls: + enable: true + caCertPath: "/home/user/.docker/ca.pem" + certPath: "/home/user/.docker/cert.pem" + keyPath: "/home/user/.docker/key.pem" + host: "docker.example.com" + insecureSkipVerify: false +``` + +### Remote Docker Daemon Setup + +When connecting to a remote Docker daemon with TLS: + +1. **Set the Docker Host**: Use the `DOCKER_HOST` environment variable to specify where to connect: + ```bash + export DOCKER_HOST=tcp://docker.example.com:2376 + ``` + +2. **Configure TLS**: Update your `config.yml`: + ```yaml + tls: + enable: true + caCertPath: "/path/to/ca.pem" + certPath: "/path/to/cert.pem" + keyPath: "/path/to/key.pem" + host: "docker.example.com" # Must match certificate + insecureSkipVerify: false + ``` + +### Important Notes + +1. **Don't confuse two different "hosts"**: + - `DOCKER_HOST` environment variable: **WHERE** to connect (e.g., `tcp://192.168.1.100:2376`) + - `tls.host` config field: **WHAT NAME** to expect on the certificate (e.g., `docker.example.com`) + +2. **Certificate Validation**: The `host` field must match a name in your Docker daemon's certificate. You can check your certificate's valid names with: + ```bash + openssl x509 -in server-cert.pem -noout -text + ``` + +3. **Certificate Paths**: Ensure all certificate files are readable by the user running Lazydocker + +### Testing TLS Configuration + +For testing purposes only, you can skip certificate verification: + +```yaml +tls: + enable: true + caCertPath: "/path/to/ca.pem" + certPath: "/path/to/cert.pem" + keyPath: "/path/to/key.pem" + host: "any-name-here" # Ignored when insecureSkipVerify is true + insecureSkipVerify: true # INSECURE - only for testing! +``` + +### Common TLS Errors + +- **"x509: certificate is valid for X, not Y"**: The `host` field doesn't match any name on the server's certificate +- **"certificate signed by unknown authority"**: The `caCertPath` is incorrect or the server's certificate wasn't signed by the specified CA +- **"no such host"**: Network connectivity issue with `DOCKER_HOST` (not a TLS config problem) +- **"tls: failed to verify certificate"**: General certificate validation failure - check all certificate paths and the `host` field + ## Color Attributes: For color attributes you can choose an array of attributes (with max one color attribute) diff --git a/pkg/commands/docker.go b/pkg/commands/docker.go index 28c12c23..0bb2bb21 100644 --- a/pkg/commands/docker.go +++ b/pkg/commands/docker.go @@ -7,6 +7,7 @@ import ( "fmt" "io" ogLog "log" + "net/http" "os" "os/exec" "strings" @@ -18,6 +19,7 @@ import ( ctxstore "github.com/docker/cli/cli/context/store" "github.com/docker/docker/api/types/container" "github.com/docker/docker/client" + "github.com/docker/go-connections/tlsconfig" "github.com/imdario/mergo" "github.com/jesseduffield/lazydocker/pkg/commands/ssh" "github.com/jesseduffield/lazydocker/pkg/config" @@ -97,7 +99,38 @@ func NewDockerCommand(log *logrus.Entry, osCommand *OSCommand, tr *i18n.Translat dockerHost = dockerHostFromEnv } - cli, err := client.NewClientWithOpts(client.FromEnv, client.WithVersion(APIVersion), client.WithHost(dockerHost)) + opts := []client.Opt{ + client.FromEnv, + client.WithVersion(APIVersion), + client.WithHost(dockerHost), + } + + if config.UserConfig.TLS.Enable { + tlsConfigOpts := tlsconfig.Options{ + CAFile: config.UserConfig.TLS.CACertPath, + CertFile: config.UserConfig.TLS.CertPath, + KeyFile: config.UserConfig.TLS.KeyPath, + InsecureSkipVerify: config.UserConfig.TLS.InsecureSkipVerify, + ExclusiveRootPools: true, + } + customTLSConfig, err := tlsconfig.Client(tlsConfigOpts) + if err != nil { + ogLog.Fatalf("Failed to create custom TLS config: %v", err) + } + + if config.UserConfig.TLS.Host != "" { + customTLSConfig.ServerName = config.UserConfig.TLS.Host + } + httpClient := &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: customTLSConfig, + }, + CheckRedirect: client.CheckRedirect, + } + opts = append(opts, client.WithHTTPClient(httpClient)) + } + + cli, err := client.NewClientWithOpts(opts...) if err != nil { ogLog.Fatal(err) } diff --git a/pkg/config/app_config.go b/pkg/config/app_config.go index 9c0ad474..79284231 100644 --- a/pkg/config/app_config.go +++ b/pkg/config/app_config.go @@ -32,6 +32,9 @@ type UserConfig struct { // hit esc or q when no confirmation panels are open ConfirmOnQuit bool `yaml:"confirmOnQuit,omitempty"` + // TLS configuration for connecting to Docker daemon + TLS TLSConfig `yaml:"tls,omitempty"` + // Logs determines how we render/filter a container's logs Logs LogsConfig `yaml:"logs,omitempty"` @@ -346,6 +349,35 @@ type LogsConfig struct { Tail string `yaml:"tail,omitempty"` } +// TLSConfig holds TLS configuration for connecting to Docker daemon +type TLSConfig struct { + // Enable TLS connections to Docker daemon + Enable bool `yaml:"enable,omitempty"` + + // CACertPath is the path to the Certificate Authority (CA) certificate file + // used to verify the Docker daemon's server certificate + CACertPath string `yaml:"caCertPath,omitempty"` + + // CertPath is the path to the client certificate file for mutual TLS authentication + CertPath string `yaml:"certPath,omitempty"` + + // KeyPath is the path to the client private key file corresponding to the client certificate + KeyPath string `yaml:"keyPath,omitempty"` + + // InsecureSkipVerify controls whether a client verifies the server's certificate chain + // and host name. If InsecureSkipVerify is true, TLS accepts any certificate presented + // by the server and any host name in that certificate. This should only be used for + // testing purposes as it makes TLS susceptible to man-in-the-middle attacks. + InsecureSkipVerify bool `yaml:"insecureSkipVerify,omitempty"` + + // Host specifies the hostname or IP address expected on the Docker daemon's certificate. + // This value is used for Server Name Indication (SNI) and certificate validation. + // It must match either the Common Name (CN) or one of the Subject Alternative Names (SANs) + // in the server's certificate. Do not include protocol (tcp://) or port - just the hostname/IP. + // Example: "docker.example.com" or "192.168.1.100" + Host string `yaml:"host,omitempty"` +} + // GetDefaultConfig returns the application default configuration NOTE (to // contributors, not users): do not default a boolean to true, because false is // the boolean zero value and this will be ignored when parsing the user's @@ -474,6 +506,14 @@ func GetDefaultConfig() UserConfig { Replacements: Replacements{ ImageNamePrefixes: map[string]string{}, }, + TLS: TLSConfig{ + Enable: false, + CACertPath: "", + CertPath: "", + KeyPath: "", + InsecureSkipVerify: false, + Host: "", + }, } }