> ## Documentation Index
> Fetch the complete documentation index at: https://docs.nebius.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Receiving webhooks in a private service by using Nebius Tunnels

Webhook providers such as GitHub, Stripe and Telegram send requests to publicly reachable HTTPS endpoints. A webhook handler running on a virtual machine (VM) without a public IP address cannot receive these requests directly. Nebius Tunnels provides a public HTTPS endpoint for the webhook handler. A tunnel agent running on the VM establishes an outbound connection to Nebius Tunnels. When a webhook provider sends a request to the public endpoint, Nebius Tunnels forwards it through this connection to the handler on the VM. The VM does not need an open inbound firewall port. The tunnel URL remains stable for the lifetime of the tunnel. You can configure this URL as the webhook endpoint in the external service and continue using the same URL for subsequent requests.

When the VM starts, the tunnel agent starts automatically and connects to the tunnel. Requests sent to the public tunnel URL are forwarded through this connection to the service running on the VM.

## Limitations

Before creating the infrastructure, consider the following:

* Connect only one VM to each tunnel. Connecting multiple VMs to the same tunnel is not supported.
* Nebius Tunnels forwards incoming requests but does not authenticate their senders. Configure your webhook handler to verify the signature provided by the webhook provider.

## Costs

Nebius AI Cloud charges you for the following billing items:

* [Compute virtual machine](/compute/resources/pricing#virtual-machines-gpus-vcpus-ram)
* [Boot disk](/compute/resources/pricing#disks) attached to the VM

Nebius Tunnels is [in preview](/overview/services#service-and-application-stages) and provided free of charge.

## Steps

### Create a tunnel

<Tabs group="interfaces">
  <Tab title="Web console">
    1. In the [web console](https://console.nebius.com), select the project where you want to create the infrastructure. All resources must be located in the same project.
    2. Go to <Icon icon="https://mintcdn.com/nebius-ai-cloud/rOlLZ_MFvrheaI-h/_assets/sidebar/networking.svg?fit=max&auto=format&n=rOlLZ_MFvrheaI-h&q=85&s=7a4fe6d935f68131371d7b49a9bd595f" width="16" height="16" data-path="_assets/sidebar/networking.svg" /> **Networking** → **Tunnels**.
    3. Click <Icon icon="https://mintcdn.com/nebius-ai-cloud/1Ha0sWR6e1mnIaHS/_assets/plus.svg?fit=max&auto=format&n=1Ha0sWR6e1mnIaHS&q=85&s=7c9efc69d65fc58db0eb73702fd81aa1" width="16" height="16" data-path="_assets/plus.svg" /> **Create tunnel**.
    4. On the page that opens, set the tunnel name to `my-tunnel`.
    5. Click **Create tunnel**.
  </Tab>

  <Tab title="CLI">
    1. [Install and configure](/cli/install) the Nebius AI Cloud CLI.

       When configuring the CLI profile, set the project where you want to create the infrastructure. All resources must be located in the same project.

       All `nebius` commands by default use the [project ID](/iam/manage-projects#how-to-get-a-project-id) from your CLI profile configuration.

    2. Create a tunnel:

       ```bash theme={null}
       nebius tunnel create --title my-tunnel 
       ```
  </Tab>
</Tabs>

### Configure access for the tunnel agent

To authorize the VM in the tunnel, prepare a service account with the `applicationtunnel.agent` role. Later, you attach this service account to the VM.

1. Create the service account:

   <Tabs group="interfaces">
     <Tab title="Web console">
       1. In the sidebar, go to <Icon icon="https://mintcdn.com/nebius-ai-cloud/1Ha0sWR6e1mnIaHS/_assets/sidebar/administration.svg?fit=max&auto=format&n=1Ha0sWR6e1mnIaHS&q=85&s=e6411dc023fd6972922c0a12a59ccf21" width="16" height="16" data-path="_assets/sidebar/administration.svg" /> **Administration** → **IAM**.
       2. Click **Create resource** → **Service account**.
       3. In the window that opens, set the name to `tunnel-agent-sa` and select the project.
       4. Click **Create and continue**.
     </Tab>

     <Tab title="CLI">
       ```bash theme={null}
       nebius iam service-account create --name tunnel-agent-sa 
       ```
     </Tab>
   </Tabs>

2. Create a [custom group](/iam/authorization/groups/index#custom-groups) in Identity and Access Management to assign the role to the service account:

   <Tabs group="interfaces">
     <Tab title="Web console">
       1. In the sidebar, go to <Icon icon="https://mintcdn.com/nebius-ai-cloud/1Ha0sWR6e1mnIaHS/_assets/sidebar/administration.svg?fit=max&auto=format&n=1Ha0sWR6e1mnIaHS&q=85&s=e6411dc023fd6972922c0a12a59ccf21" width="16" height="16" data-path="_assets/sidebar/administration.svg" /> **Administration** → **IAM**.
       2. Click **Create resource** → **Group**.
       3. In the window that opens, set the group name to `tunnel-agents`. In the **Scope** field, select the project where you create all the resources.
       4. Click **Create**.
     </Tab>

     <Tab title="CLI">
       ```bash theme={null}
       nebius iam group create --name tunnel-agents 
       ```
     </Tab>
   </Tabs>

3. Attach the tunnel and grant the `applicationtunnel.agent` role to this group. You can only grant this role by using the CLI.

   <Tabs group="interfaces">
     <Tab title="CLI">
       1. Get the group ID:

          ```bash theme={null}
          nebius iam group list
          ```

          The ID is specified in the `items.metadata.id` parameter in the output.

       2. Get the tunnel ID:

          ```bash theme={null}
          nebius tunnel list
          ```

       3. Create an access permit in the custom group:

          ```bash theme={null}
          nebius iam access-permit create \
            --parent-id <group_ID> \
            --resource-id <tunnel_ID> \
            --role applicationtunnel.agent
          ```
     </Tab>
   </Tabs>

4. Add the service account to the group:

   <Tabs group="interfaces">
     <Tab title="Web console">
       1. Go to <Icon icon="https://mintcdn.com/nebius-ai-cloud/1Ha0sWR6e1mnIaHS/_assets/sidebar/administration.svg?fit=max&auto=format&n=1Ha0sWR6e1mnIaHS&q=85&s=e6411dc023fd6972922c0a12a59ccf21" width="16" height="16" data-path="_assets/sidebar/administration.svg" /> **Administration** → **IAM**.
       2. On the **Groups** tab, find and open the `tunnel-agents` group.
       3. Click <Icon icon="https://mintcdn.com/nebius-ai-cloud/1Ha0sWR6e1mnIaHS/_assets/plus.svg?fit=max&auto=format&n=1Ha0sWR6e1mnIaHS&q=85&s=7c9efc69d65fc58db0eb73702fd81aa1" width="16" height="16" data-path="_assets/plus.svg" /> **Add members**.
       4. In the window that opens, add the `tunnel-agent-sa` service account.
       5. Once the account is added, close the window.

       Check the group page in general. Make sure that:

       * The `tunnel-agent-sa` service account is specified on the **Members** tab.
       * The tunnel and the `applicationtunnel.agent` role are specified on the **Access permits** tab.
     </Tab>

     <Tab title="CLI">
       ```bash theme={null}
       nebius iam group-membership create \
         --parent-id <group_ID> \
         --member-id <service_account_ID>
       ```

       To get the service account ID, run:

       ```bash theme={null}
       nebius iam service-account list
       ```
     </Tab>
   </Tabs>

### Create a virtual machine

Create a VM without a public IP address and use a cloud-init configuration to install and start the tunnel agent and a sample webhook handler. Attach the service account you configured earlier so that the tunnel agent can connect to the tunnel.

<Warning>
  If you need to change the VM configuration later, update `webhook.yaml`, delete the VM and create a new one. A tunnel should be associated with only one VM, and you can't change the cloud-init configuration of an existing VM.
</Warning>

1. [Generate an SSH key pair](/compute/virtual-machines/ssh-keys#generating-a-key-pair).

2. Prepare the cloud-init configuration file. You'll attach it to the VM.

   In this file, specify two parameters:

   * `users.ssh_authorized_keys`: Public SSH key.
   * `write_files.content.tunnel_id`: ID of the tunnel.

   <Accordion title="webhook.yaml">
     ```yaml theme={null}
     #cloud-config
     users:
       - name: ubuntu
         sudo: "ALL=(ALL) NOPASSWD:ALL"
         shell: /bin/bash
         ssh_authorized_keys:
           - <public_SSH_key>

     # Driverless images don't include Docker, so install it during VM initialization.
     package_update: true
     packages:
       - docker.io

     write_files:
       - path: /etc/nebius-tunnel-agent/config.yaml
         permissions: "0640"
         content: |
           tunnel_id: <tunnel_ID>

           iam:
             metadata:
               enabled: true

           logging:
             level: info
             format: json

           services:
             - name: hook
               type: http
               target: 127.0.0.1:8080
             - name: ssh
               type: tcp
               target: 127.0.0.1:22

       - path: /etc/systemd/system/nebius-tunnel-agent.service
         permissions: "0644"
         content: |
           [Unit]
           Description=Nebius tunnel agent
           Documentation=https://docs.nebius.com/tunnels
           After=network-online.target
           Wants=network-online.target

           [Service]
           Type=simple
           User=tunna
           Group=tunna
           ExecStart=/usr/local/bin/nebius-tunnel-agent --config /etc/nebius-tunnel-agent/config.yaml
           Restart=always
           RestartSec=5
           NoNewPrivileges=true
           ProtectSystem=strict
           ProtectHome=true
           PrivateTmp=true
           ReadOnlyPaths=/etc/nebius-tunnel-agent

           [Install]
           WantedBy=multi-user.target

       - path: /usr/local/sbin/install-nebius-tunnel-agent
         permissions: "0755"
         content: |
           #!/bin/bash
           set -euo pipefail

           VERSION="${AGENT_VERSION:-latest}"
           BASE="https://storage.eu-north1.nebius.cloud/products/releases/nebius-tunnel-agent"
           CONF_DIR=/etc/nebius-tunnel-agent

           case "$(uname -m)" in
             x86_64)        PLATFORM=linux-x86_64 ;;
             aarch64|arm64) PLATFORM=linux-aarch64 ;;
             *) echo "unsupported architecture: $(uname -m)" >&2; exit 1 ;;
           esac
           ASSET="nebius-tunnel-agent-${PLATFORM}.tar.gz"

           TMP="$(mktemp -d)"
           trap 'rm -rf "$TMP"' EXIT

           fetch() {
             curl -fsSL --retry 5 --retry-delay 3 --retry-connrefused -o "$2" "$1"
           }
           fetch "$BASE/$VERSION/$ASSET" "$TMP/$ASSET"
           fetch "$BASE/$VERSION/checksums-sha256.txt" "$TMP/checksums-sha256.txt"

           (cd "$TMP" && sha256sum --ignore-missing -c checksums-sha256.txt)

           tar -xzf "$TMP/$ASSET" -C "$TMP"
           install -m0755 "$TMP/nebius-tunnel-agent" /usr/local/bin/nebius-tunnel-agent

           getent group tunna >/dev/null || groupadd --system tunna
           id -u tunna >/dev/null 2>&1 || useradd --system --gid tunna \
             --no-create-home --shell /usr/sbin/nologin tunna
           chown -R tunna:tunna "$CONF_DIR"
           chmod 0750 "$CONF_DIR"
           chmod 0640 "$CONF_DIR/config.yaml"

           systemctl daemon-reload
           systemctl enable nebius-tunnel-agent.service
           systemctl restart nebius-tunnel-agent.service

       - path: /etc/systemd/system/webhook-sink.service
         permissions: "0644"
         content: |
           [Unit]
           Description=go-httpbin webhook sink (behind a Nebius tunnel)
           After=network-online.target docker.service
           Wants=network-online.target
           Requires=docker.service

           [Service]
           ExecStartPre=-/usr/bin/docker rm -f webhook-sink
           ExecStartPre=/usr/bin/docker pull ghcr.io/mccutchen/go-httpbin:2.22
           ExecStart=/usr/bin/docker run --rm --name webhook-sink \
             -p 127.0.0.1:8080:8080 \
             ghcr.io/mccutchen/go-httpbin:2.22
           ExecStop=/usr/bin/docker stop webhook-sink
           Restart=on-failure
           RestartSec=10
           TimeoutStartSec=0

           [Install]
           WantedBy=multi-user.target

     runcmd:
       # Agent first: the tunnel is up while the backend is still pulling images.
       - [/usr/local/sbin/install-nebius-tunnel-agent]
       - systemctl enable --now webhook-sink.service
     ```

     This configuration enables the following actions:

     * Add a username and a public SSH key to the VM.

     * Install Docker on the VM.

     * Authenticate by using the service account attached to the VM.

     * Create two [Nebius Tunnels services](/tunnels/overview#supported-traffic):

       * `hook`: To receive incoming webhook requests on port 8080.
       * `ssh`: To connect to the VM by using SSH.

     * Deploy the tunnel agent on the VM.

     * Deploy `go-httpbin` as a placeholder webhook receiver. Replace it with your own service and point `target` at its port.
   </Accordion>

3. Create the VM:

   <Tabs group="interfaces">
     <Tab title="Web console">
       1. In the sidebar, go to <Icon icon="https://mintcdn.com/nebius-ai-cloud/rOlLZ_MFvrheaI-h/_assets/sidebar/compute.svg?fit=max&auto=format&n=rOlLZ_MFvrheaI-h&q=85&s=8d3eda9b92f5a626a81d01268852f482" width="16" height="16" data-path="_assets/sidebar/compute.svg" /> **Compute** → **Virtual machines**.

       2. Click **Create resource** → **Virtual machine**.

          The creation flow is a step-by-step wizard. The sidebar shows your progress through the configuration sections. To move between sections, click **Back** and **Next**.

       3. On the **Compute** step, configure computing resources:

          1. In the **Platform** section, select:

             * Without GPUs
             * Regular
             * Non-GPU AMD Epyc Genoa

          2. In the **Settings** section, set:

             * **Preset**: 2 CPUs - 8 GiB RAM.
             * **Project**: The project where you create all resources for the tunnel.
             * **VM name**: `tunnel-hook`.

       4. On the **Storage** step, preserve the **Ubuntu 24.04 LTS** disk image and the SSD disk type, but change the disk size to 20 GiB.

       5. On the **Network** step, check the network and subnet. Keep **None** in the **Public IP address** field.

       6. On the **Configuration** step, configure the access, identity and VM startup settings:

          1. In the **Service account** field, select the `tunnel-agent-sa` service account.
          2. Enable a custom cloud-init configuration file.
          3. In the field that appears, paste the contents of the `webhook.yaml` file prepared earlier.

       7. On the **Review** step, check the full VM configuration. To change a section quickly, click <Icon icon="https://mintcdn.com/nebius-ai-cloud/1Ha0sWR6e1mnIaHS/_assets/pencil-to-line.svg?fit=max&auto=format&n=1Ha0sWR6e1mnIaHS&q=85&s=9f557f28487b7fec78ae7508b5b6a145" width="16" height="16" data-path="_assets/pencil-to-line.svg" /> next to the corresponding block. The wizard opens the relevant step with your current settings.

       8. Click **Create VM**.
     </Tab>

     <Tab title="CLI">
       ```bash theme={null}
       nebius compute instance create \
         --name tunnel-hook \
         --resources-platform cpu-e2 \
         --resources-preset 2vcpu-8gb \
         --boot-disk-attach-mode READ_WRITE \
         --boot-disk-managed-disk-name tunnel-hook-boot \
         --boot-disk-managed-disk-type network_ssd \
         --boot-disk-managed-disk-size-gibibytes 20 \
         --boot-disk-managed-disk-source-image-family-image-family ubuntu24.04-driverless \
         --network-interfaces "[{\"name\": \"eth0\", \"ip_address\": {}, \"subnet_id\": \"<subnet_ID>\"}]" \
         --service-account-id <service_account_ID> \
         --cloud-init-user-data "$(cat webhook.yaml)"
       ```

       In the command, specify the [subnet ID](/vpc/networking/resources#how-to-get-a-subnet-id) and the ID of the `tunnel-agent-sa` service account.
     </Tab>
   </Tabs>

Wait for the VM to start running.

### Check the result

The tunnel agent starts as soon as the VM boots. The `webhook-sink` service starts shortly after Docker pulls the `go-httpbin` image.

To check that the webhook endpoint is working, send a test request from your local machine:

```bash theme={null}
curl -X POST \
  https://hook-<tunnel_masked_ID>.tunnel.applications.<region>.nebius.cloud/anything \
  -H "Content-Type: application/json" \
  -d '{"ping": 1}'
```

In the URL, specify:

* `tunnel_masked_ID`: Mask of the tunnel ID without the `applicationtunnel-` prefix and regional routing code. Contains the last 15 characters of the tunnel ID. For example, for `applicationtunnel-e00abcdef123456789`, the masked ID of the tunnel is `abcdef123456789`.
* `region`: [Region](/overview/regions) of the project where you created the resources.

For more information about the tunnel URL, see [Supported traffic](/tunnels/overview#supported-traffic).

The expected response is a JSON object that echoes back the request details:

```json theme={null}
{
  "method": "POST",
  "url": "https://hook-<tunnel_masked_ID>.tunnel.applications.<region>.nebius.cloud/anything",
  "data": "{\"ping\": 1}",
  ...
}
```

If the response is `failed to connect to local service`, the `webhook-sink` service hasn't started yet. Wait a few moments and try again. If it still fails, see [Troubleshooting](#troubleshooting).

Once the endpoint is working, paste the tunnel URL — `https://hook-<tunnel_masked_ID>.tunnel.applications.<region>.nebius.cloud` — into the webhook settings of your provider (GitHub, Stripe, Telegram, etc.). The tunnel URL is stable for the tunnel's lifetime, so you only need to do this once.

To replace `go-httpbin` with your own webhook handler, update `webhook.yaml`, delete the VM and create it again. Configure your handler to listen on port 8080, or change the tunnel agent's `target` to match your handler's port.

## Troubleshooting

If the test request fails, or the tunnel returns `failed to connect to local service`, connect to the VM and check the tunnel agent and the webhook handler. The VM has no public IP address. Connect through the `ssh` service of the tunnel.

### Connect to the VM

Connect to the VM as described in [Connecting to a virtual machine without a public IP address](/tunnels/tutorials/connect-vm#connect-to-the-virtual-machine).

If you still can't open the session, use the [serial console](/compute/virtual-machines/serial-console) and run the checks below from there.

### Check the tunnel agent

On the VM, check the agent service and its logs:

```bash theme={null}
sudo systemctl status nebius-tunnel-agent.service
sudo journalctl -u nebius-tunnel-agent.service
```

Add `-f` to follow new log lines, for example `sudo journalctl -u nebius-tunnel-agent.service -f`.

When the agent has registered with the tunnel, the logs include the public endpoint of each service.

If the agent doesn't register, check for these common causes:

* **The VM has no service account attached.** `systemctl status` still shows `active (running)`, but the agent retries indefinitely, showing `IMDS token request failed: 404 Not Found` followed by `connection closed before registering`. You can't attach a service account to an existing VM. Delete the VM and create a new one with the `tunnel-agent-sa` service account attached.
* **The tunnel agent isn't authorized to connect to the tunnel.** Verify that the `tunnel-agent-sa` service account belongs to the `tunnel-agents` group and that the group includes the `applicationtunnel.agent` role for this tunnel. Check the steps in [Configure access for the tunnel agent](#configure-access-for-the-tunnel-agent).

Check that `tunnel_id` in the agent configuration is the ID of the tunnel you created:

```bash theme={null}
sudo grep tunnel_id /etc/nebius-tunnel-agent/config.yaml
```

The value looks like `applicationtunnel-e00abcdef123456789`. If it is still the `<tunnel_ID>` placeholder, or it points at a different tunnel, delete the VM and create it again with the correct tunnel ID in `webhook.yaml`.

### Check the webhook handler

The `hook` service forwards requests to `webhook-sink` on port 8080. Check that service and its logs:

```bash theme={null}
sudo systemctl status webhook-sink.service
sudo journalctl -u webhook-sink.service
```

`webhook-sink` runs `go-httpbin` in Docker. If the unit is active and requests still fail, check the container:

```bash theme={null}
sudo docker ps -a --filter name=webhook-sink
sudo docker logs webhook-sink
```

The first command should return a container named `webhook-sink`. Its status should begin with `Up`, and the port mapping should be `127.0.0.1:8080->8080/tcp`. This means that the container is running and its port `8080` is available through the VM's loopback interface.

The output of the second command should contain the following message:

```text theme={null}
go-httpbin listening on http://0.0.0.0:8080
```

### Check cloud-init

Use this check if either `systemctl status` command reports that the `nebius-tunnel-agent.service` or `webhook-sink.service` unit could not be found. This means that cloud-init might not have completed successfully.

Check the cloud-init output:

```bash theme={null}
sudo tail -n 200 /var/log/cloud-init-output.log
```

The log records the installation of the tunnel agent and the startup of `webhook-sink.service`. Check it for errors related to downloading or installing the tunnel agent, installing Docker or starting the webhook handler. After resolving the error, delete the VM and create it again with the corrected `webhook.yaml` file because cloud-init runs only when the VM is created.

## How to delete the created resources

The created VM and its boot disk are chargeable. If you don't need them, [delete the VM](/compute/virtual-machines/delete), so Nebius AI Cloud doesn't charge for the resources. The boot disk is deleted automatically when you delete the VM.

You can also delete free resources:

* [Tunnel](/tunnels/delete)
* [Service account](/iam/service-accounts/manage#deleting-a-service-account)
* [Custom group](/iam/authorization/groups/delete-groups)
