Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
This is a Home Assistant custom integration that monitors Docker containers via a Docker socket proxy API. It uses the coordinator pattern for data fetching and dynamic sensor entities.

**Key Components:**
- `coordinator.py`: Fetches data from `/containers/json` and `/version` endpoints in parallel
- `coordinator.py`: Fetches data from `/version` and `/containers/json` sequentially; passes the optional SSL context via `ssl=` and logs each distinct failure reason once
- `tls.py`: Builds an `ssl.SSLContext` from PEM text in the config entry (optional private CA, mTLS client cert/key/passphrase). Server verification is always on; there is intentionally no skip-verify option
- `diagnostics.py`: Redacts keys, passphrases, certificate PEMs and the proxy host
- `sensor.py`: Creates host sensor (running/total ratio) and per-container sensors with detailed attributes
- `config_flow.py`: Handles setup with URL validation via `/version` endpoint
- `config_flow.py`: Setup and in-place Reconfigure (URL + TLS file uploads) with validation via `/version`; reconfigure keeps the config entry so entities and history survive, and updates the entry's unique ID (the URL)
- Dynamic entity management with grace period cleanup for orphaned containers

## Data Flow
Expand All @@ -25,6 +27,7 @@ This is a Home Assistant custom integration that monitors Docker containers via
- Supports multiple Docker hosts via separate config entries
- Options flow for grace period settings (enabled/disabled, seconds)
- URL validation in config flow ensures proxy connectivity
- TLS fields are optional; entries saved before TLS support (no TLS keys) must keep working unchanged

## Development Notes
- No external dependencies; uses aiohttp for API calls
Expand All @@ -33,4 +36,4 @@ This is a Home Assistant custom integration that monitors Docker containers via
- Container names stripped of leading `/` for consistency

## Testing
Run Home Assistant with the integration loaded; verify sensors appear for containers and update on state changes.
`pip install -r requirements_test.txt && pytest` (Python 3.13+). Tests live in `tests/` and use `pytest-homeassistant-custom-component`; `tests/pki.py` generates throwaway certificates in memory and `test_integration.py` talks to a real local mTLS server. Also verify manually that sensors appear for containers and update on state changes.
34 changes: 34 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: Tests

on:
workflow_dispatch:
push:
branches:
- main
pull_request:
branches:
- main

permissions:
contents: read

jobs:
pytest:
name: pytest
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1

- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.13"
cache: pip
cache-dependency-path: requirements_test.txt

- name: Install test dependencies
run: pip install -r requirements_test.txt

- name: Run tests
run: pytest
73 changes: 72 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ A Home Assistant integration that monitors Docker containers through a Docker so
2. Search for "Docker Socket Proxy" and select it
3. Enter:
- **Instance Name**: A friendly name for this Docker host (e.g., "NAS", "Server")
- **Proxy URL**: The URL of your Docker socket proxy (e.g., `http://192.168.1.100:2375`)
- **Proxy URL**: The URL of your Docker socket proxy (e.g., `http://192.168.1.100:2375`, or `https://docker.example.com:2376` for a TLS-protected proxy)
- *(Optional)* **CA certificate**, **Client certificate**, **Client private key** and **Client key passphrase**: only needed for an HTTPS proxy that uses a private CA and/or requires client certificates. See [Securing the connection (TLS / mutual TLS)](#securing-the-connection-tls--mutual-tls).

### Configuration & Options

Expand Down Expand Up @@ -94,6 +95,58 @@ services:
start_period: "21s"
```

### Securing the connection (TLS / mutual TLS)

The integration can talk to a proxy that is protected with HTTPS, optionally with
**mutual TLS (mTLS)**, where the proxy only accepts clients that present a
certificate from a CA you trust. This is the recommended way to expose a proxy
beyond `localhost`, especially if it allows more than read-only access.

[Tecnativa's docker-socket-proxy](https://github.com/Tecnativa/docker-socket-proxy)
does not speak TLS itself, so put a TLS terminator in front of it (nginx, Caddy,
HAProxy, Traefik, ...). Minimal nginx example, run next to the proxy container
(`dockerproxy` here is the proxy's container name):

```nginx
events {}
stream {
server {
listen 2376 ssl;
proxy_pass dockerproxy:2375;

ssl_certificate /certs/server.pem;
ssl_certificate_key /certs/server.key;
ssl_protocols TLSv1.2 TLSv1.3;

# Remove the next two lines for server-side TLS only (no client certificate)
ssl_client_certificate /certs/ca.pem;
ssl_verify_client on;
}
}
```

Then, in Home Assistant, use an `https://` URL and upload the files:

| Field | What to upload | When |
| :--- | :--- | :--- |
| **CA certificate** | PEM of the CA that signed the proxy's certificate (or the proxy's own certificate if self-signed) | Only if the proxy certificate is not signed by a publicly trusted CA |
| **Client certificate** | PEM client certificate | Only if the proxy requires mTLS |
| **Client private key** | PEM key matching the client certificate | Required together with the client certificate |
| **Client key passphrase** | Passphrase | Only if the private key is encrypted |

Notes:

- The proxy's certificate is **always verified**, and there is deliberately no "skip verification" switch. The host name in the URL must match a name or IP address in the certificate's *Subject Alternative Name* list.
- Leave all TLS fields empty for plain `http://` or for HTTPS with a publicly trusted certificate; behaviour is then unchanged.
- The uploaded certificates and key are stored in the integration's config entry, like other credentials in Home Assistant. They are redacted in diagnostics downloads and never logged.
- TLS 1.2 or newer is required.

To add TLS to an **existing** entry without losing any entities or history, open
**Settings → Devices & Services → Docker Socket Proxy**, use the three-dot menu
of the entry and choose **Reconfigure**. You can change the URL and upload,
replace or remove (**Remove stored TLS certificates**) the certificate files.
Leaving a file field empty keeps the stored file.

### Container Labels

Add labels to your containers for enhanced functionality:
Expand Down Expand Up @@ -409,6 +462,16 @@ Automatically receive push notifications on your phone if:
- Check that the docker-socket-proxy container is running
- Ensure firewall allows connections to the proxy port

#### TLS errors

Check the Home Assistant log for `custom_components.docker_socket_proxy`; it states the reason once per outage.

- **"TLS certificate verification failed"**: the CA certificate does not match the CA that signed the proxy's certificate, or the host name in the URL is not listed in the certificate's Subject Alternative Names. Use the name or IP the certificate was issued for.
- **"connection closed by the server ... requires a client certificate"**: the proxy wants mutual TLS but no (or an unaccepted) client certificate was sent. Upload a client certificate signed by the CA the proxy trusts.
- **"The client certificate and private key do not belong together"**: re-upload the matching pair.
- **"HTTP 403"**: TLS works, but the proxy denies that API endpoint. Enable it in the proxy (for Tecnativa: `CONTAINERS=1`, `VERSION=1`).
- Expired certificates also fail verification; the certificate's expiry date is included in the diagnostics download.

#### Missing containers

- Containers must be running for auto-discovery
Expand All @@ -433,6 +496,14 @@ logger:

## Contributing

Tests run with `pytest` (Python 3.13 or newer):

```bash
pip install -r requirements_test.txt
pytest
```


Contributions are welcome! Please:

1. Fork the repository
Expand Down
15 changes: 14 additions & 1 deletion custom_components/docker_socket_proxy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@

from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryError

# Import constants and the coordinator class
from .const import BLUEPRINT_FILENAME, DOMAIN, PLATFORMS
from .coordinator import DockerProxyCoordinator
from .tls import DockerProxyTLSError, build_ssl_context

_LOGGER = logging.getLogger(__name__)

Expand All @@ -28,9 +30,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
# Deploy or update the blueprint before setting up platforms to ensure it's available
await async_install_blueprint(hass)

# Build the TLS context (custom CA / client certificate) off the event loop.
# It is None for plain http or default-trust https, which keeps the previous
# behaviour. Stored material that no longer loads is a configuration error
# the user must fix via Reconfigure, so do not retry it endlessly.
try:
ssl_context = await hass.async_add_executor_job(build_ssl_context, entry.data)
except DockerProxyTLSError as err:
raise ConfigEntryError(
f"Invalid TLS configuration ({err.reason}); use Reconfigure to fix it"
) from err

# Initialize the coordinator with the full entry object
# This allows the coordinator to access entry.options and entry.data["url"]
coordinator = DockerProxyCoordinator(hass, entry)
coordinator = DockerProxyCoordinator(hass, entry, ssl_context)

# Trigger the first refresh to ensure we have data before creating entities
# This will set the initial host_status and populate the container list
Expand Down
Loading
Loading