> For the complete documentation index, see [llms.txt](https://docs.datasentinel.io/manual/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.datasentinel.io/manual/ai-connector/troubleshooting/tls-hostname-mismatch.md).

# TLS hostname mismatch on API calls

Every MCP tool call fails with "Hostname mismatch" during the TLS handshake. Set DS\_MCP\_PUBLIC\_URL on the datasentinel\_mcp service to fix it.

**Applies to:** a self-hosted Datasentinel MCP server (the `datasentinel_mcp` service) whose tool calls fail during the TLS handshake with the API.

{% hint style="info" %}
Throughout this page, replace `datasentinel.example.com` with the fully-qualified public hostname of your own Datasentinel API: the name your TLS certificate was issued for.
{% endhint %}

## Symptom

Every Datasentinel MCP tool call fails immediately, before returning any data:

```
[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed:
Hostname mismatch, certificate is not valid for '<short-hostname>'. (_ssl.c:1000)
```

The name in the error is the **short** hostname of the machine running the MCP server, the bare label with no domain attached. The failure is total: no instance list, no analysis, no platform status.

## Reading the error

Two details identify the failure precisely.

**The name is short, not fully-qualified.** A public certificate can never cover a bare label: Let's Encrypt and other public CAs will not issue for a name that isn't publicly resolvable. So whatever built the URL used the machine's short hostname rather than the API's public name.

**`_ssl.c:1000` means the failure is in Python.** That is CPython's SSL module. If your MCP client uses `mcp-remote` (Node), a client-side TLS failure would surface as `ERR_TLS_CERT_ALTNAME_INVALID` or `UNABLE_TO_VERIFY_LEAF_SIGNATURE`, never a `_ssl.c` line number. The failing handshake is therefore inside the **Python** MCP server process, on its onward call to the Datasentinel API.

There are two legs, and only the second one is broken:

```mermaid
flowchart TD
    A["MCP client"] -->|"https://datasentinel.example.com/mcp"| B["MCP server (Python)"]
    B -->|"https://short-hostname/ds-api"| C["Datasentinel API"]
    linkStyle 0 stroke:#2f9e44,stroke-width:2px
    linkStyle 1 stroke:#e03131,stroke-width:2px
```

The first leg succeeds. The second one fails the certificate check.

This matters because it tells you where **not** to look. Check these off and move on:

* **The certificate is usually fine.** Confirm it is current and issued for your public FQDN:

  ```bash
  openssl s_client -connect datasentinel.example.com:443 </dev/null 2>/dev/null \
    | openssl x509 -noout -subject -dates
  ```

  If it is, it is not the cause.
* **The client config is usually fine.** If your `.mcp.json` (or equivalent) already points at the fully-qualified URL, the client leg is correct. The problem is downstream of it.

## Root cause

The MCP server has one deployment setting, its **public URL**: the HTTPS origin nginx serves, for example `https://datasentinel.example.com`. It drives everything that names the server, the OAuth discovery documents and login page, and also the server's own onward hop to the Datasentinel API, `<public URL>/ds-api`, which goes through the same nginx.

When the environment variable `DS_MCP_PUBLIC_URL` is not set, the server derives that URL from the machine's own identity: `https://` followed by the name `socket.getfqdn()` returns, with fallbacks when that name resolves to localhost. On a host whose fully-qualified name cannot be determined, that fallback chain yields the short hostname, which no public certificate matches.

The reason the fallback fails is specific and worth understanding, because it is not obvious:

{% hint style="warning" %}
**`socket.getfqdn()` does not consult the DNS search domain.** It resolves the hostname to an address, performs a **reverse** lookup on that address, and returns the first name in the result set that contains a dot. If nothing in that set is dotted, it returns the short name unchanged.
{% endhint %}

On a machine with no `/etc/hosts` entry and no PTR record, systemd's `nss-myhostname` module answers the lookup with a synthesised link-local or loopback address, whose reverse lookup resolves to `localhost.localdomain` or to nothing dotted at all. The fallback then degrades further and ends up at the short static hostname.

### Confirming the diagnosis

Run these on the machine running the `datasentinel_mcp` service:

```bash
hostnamectl
hostname -f
getent hosts $(hostname)
python3 -c "import socket; print(socket.gethostname(), '->', socket.getfqdn())"
systemctl show datasentinel_mcp -p Environment
```

You have confirmed this issue if:

* `hostname -f` returns the **short** name rather than an FQDN;
* `getent hosts $(hostname)` shows only a link-local (`fe80::…`) or loopback (`127.x`) address;
* `getfqdn()` returns `localhost.localdomain` or the short name;
* `DS_MCP_PUBLIC_URL` is absent from the service's environment, or empty (`DS_MCP_PUBLIC_URL=`).

## The fix

{% stepper %}
{% step %}

#### Set `DS_MCP_PUBLIC_URL` (this is the one that fixes it)

`DS_MCP_PUBLIC_URL` is the environment variable the Datasentinel MCP server reads for its public URL. It builds the API base URL from it, as `$DS_MCP_PUBLIC_URL/ds-api`, and the OAuth issuer and login URLs as well. Setting it bypasses the hostname-detection fallback entirely.

**Its value is the public HTTPS origin of your Datasentinel platform, with the exact name on the TLS certificate.** Scheme and hostname, nothing else: no path, and no port unless clients connect on one.

```
DS_MCP_PUBLIC_URL=https://datasentinel.example.com
```

Because the same value is advertised to assistants as the OAuth issuer, it must be the name they connect to. A different name here would make the sign-in fail after the certificate error is gone.

Two things to get right:

* **It must be set in the environment of the `datasentinel_mcp` service**, on the machine where that service runs. Setting it in your MCP client's config does nothing: if the client uses `mcp-remote`, that config's `env` block reaches only the local Node process, never the remote Python one.
* **It must match the certificate.** If the certificate carries several names (SANs), use the one clients actually connect to. A mismatch here produces the same error you started with.

{% tabs %}
{% tab title="systemd drop-in (recommended)" %}
The cleanest method. It survives package upgrades and reinstallations, since it doesn't touch the shipped unit file:

```bash
sudo systemctl edit datasentinel_mcp
```

In the editor that opens, add:

```ini
[Service]
Environment="DS_MCP_PUBLIC_URL=https://datasentinel.example.com"
```

Save and exit, then reload and restart:

```bash
sudo systemctl daemon-reload
sudo systemctl restart datasentinel_mcp
```

Verify it took effect, including on the OAuth side, whose discovery document must now advertise the public origin as `issuer`:

```bash
systemctl show datasentinel_mcp -p Environment
systemctl status datasentinel_mcp
curl -s https://datasentinel.example.com/.well-known/oauth-authorization-server
```

{% endtab %}

{% tab title="Environment file" %}
If you prefer to keep configuration outside systemd units, or already have a settings file for this service:

```bash
echo 'DS_MCP_PUBLIC_URL=https://datasentinel.example.com' | sudo tee -a /etc/datasentinel/mcp.env
```

Then reference it from a drop-in (`sudo systemctl edit datasentinel_mcp`):

```ini
[Service]
EnvironmentFile=/etc/datasentinel/mcp.env
```

Followed by the same `daemon-reload` and `restart`. Note that `EnvironmentFile` lines are plain `KEY=value`, with no `export` and no shell quoting.
{% endtab %}
{% endtabs %}

{% hint style="danger" %}
**Do not** set the variable in your interactive shell (`export DS_MCP_PUBLIC_URL=...`) and expect it to apply. A systemd service does not inherit your login shell's environment.
{% endhint %}
{% endstep %}

{% step %}

#### Fix the host's FQDN (worth doing anyway)

Even with `DS_MCP_PUBLIC_URL` set, a machine that cannot state its own fully-qualified name will cause confusion elsewhere: in logs, in certificates issued by ACME clients, in anything else calling `getfqdn()`.

Find the machine's real address, set the static hostname to the FQDN, and register it in `/etc/hosts`:

```bash
ip -4 addr show scope global
sudo hostnamectl set-hostname host.datasentinel.example.com
echo "10.x.x.x  host.datasentinel.example.com  host" | sudo tee -a /etc/hosts
```

**The FQDN must come first** on the `/etc/hosts` line. `getfqdn()` takes the canonical name, which is the first field after the address, so putting the short name first defeats the fix.

Verify:

```bash
python3 -c "import socket; print(socket.getfqdn())"
```

A **PTR record** for the host's address pointing at the FQDN achieves the same thing (`dig -x <address> +short` to verify), and is the better answer where you control reverse DNS. On a cloud VM behind NAT you often don't, because the address is private and any reverse record is assigned by the provider, so the `/etc/hosts` entry is the pragmatic route. `files` is consulted before `dns` in `/etc/nsswitch.conf`, so it wins either way.

{% hint style="info" %}
**What does not work:** adding `search example.com` to `/etc/resolv.conf`. The search suffix is applied only to *forward* lookups of short names, and `getfqdn()` reads the reverse direction. An A record alone is insufficient for the same reason.
{% endhint %}

If NetworkManager manages the hostname, a DHCP lease renewal may revert it. Use `hostnamectl set-hostname --static` and consider `dns=none` or an explicit `hostname-mode` setting in the NetworkManager configuration.
{% endstep %}

{% step %}

#### Remove any TLS-verification bypass

If your client config contains something like:

```json
"env": { "NODE_TLS_REJECT_UNAUTHORIZED": "0" }
```

remove it once the real fix is in place. It disables certificate validation for **every** TLS connection that Node process makes, and it does not help with this error anyway: the failing handshake is in Python, not Node.
{% endstep %}
{% endstepper %}

## Verifying the fix

Restart the service and call the platform status tool from your MCP client:

```bash
sudo systemctl restart datasentinel_mcp
```

A healthy response reports `"status": "OK"` with the backend version and every service (`nginx`, Grafana, PostgreSQL, backend, dispatcher, InfluxDB) running.

If the SSL error persists, re-check that the name in the error is now the FQDN. If it is, the mismatch is genuinely between your certificate and that name, which is a different problem from this one.

## Checklist

* [ ] Confirmed the error names the **short** hostname and cites `_ssl.c` (Python leg)
* [ ] Confirmed the certificate is valid for the public FQDN
* [ ] Confirmed `DS_MCP_PUBLIC_URL` is absent or empty in the `datasentinel_mcp` service environment
* [ ] `DS_MCP_PUBLIC_URL` set to the public HTTPS origin via a systemd drop-in or environment file
* [ ] `systemctl daemon-reload` and `systemctl restart datasentinel_mcp` run
* [ ] `systemctl show datasentinel_mcp -p Environment` shows the variable
* [ ] Host FQDN fixed (`hostnamectl` + `/etc/hosts`, FQDN first)
* [ ] `socket.getfqdn()` returns the FQDN
* [ ] Any `NODE_TLS_REJECT_UNAUTHORIZED=0` removed from the client config
* [ ] Platform status call returns OK

## Why this happens at all

The underlying design issue is that deriving a **public API endpoint** from the **machine's own identity** only works when the two happen to coincide, on a single-homed bare-metal host with correct reverse DNS. It breaks behind a load balancer, in a container, on a replica, and on any cloud VM whose internal name differs from its public one.

`DS_MCP_PUBLIC_URL` exists precisely so you don't depend on that coincidence. Set it explicitly on every deployment, including ones where the fallback currently happens to work. It costs one line and removes an entire class of failure that surfaces as an opaque TLS error rather than a configuration one.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.datasentinel.io/manual/ai-connector/troubleshooting/tls-hostname-mismatch.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
