Skip to content

P01-T10 - Cloudflare Tunnel via CHR Container

Objective

Deploy cloudflared on HQ-CHR01 using the RouterOS container feature so the router can originate a Cloudflare Tunnel while keeping access to internal servers tightly controlled.

This runbook is optional and must be treated as an add-on service, not part of the minimum routing baseline.

Validated lab status

Validated against the live lab on Friday, July 17, 2026:

  • container=yes was enabled in RouterOS device mode
  • HQ-CHR01 had to be restarted after the device-mode change before container workflows were usable
  • the container package was installed on 7.21.5
  • the dedicated container network 172.20.120.0/24 was deployed
  • cloudflared:latest reached running
  • Cloudflare prechecks passed for DNS, UDP/QUIC, TCP/HTTP2, and API reachability

Validated caveat from the same deploy:

  • the Cloudflare-side ingress configuration still pointed to ssh://172.17.0.1:22, so tunnel health was validated but the final published Windows service path was not

The intended design is:

  • a dedicated container subnet on CHR
  • explicit firewall rules from the container subnet to WAN
  • explicit firewall rules from the container subnet to SERVERS
  • optional DNS access from the container subnet to HQ-DOMAIN-CONTROLLERS

Do not give the container broad access to all internal VLANs unless a specific use case requires it.

Inputs

Key Value
Router HQ-CHR01
Container feature RouterOS container package plus device-mode container=yes
Container subnet 172.20.120.0/24
Container gateway IP 172.20.120.1/24
Container IP 172.20.120.10/24
Container interface veth-cloudflared
Bridge bridge-containers
Interface list CONTAINERS
Internal servers VLAN 172.20.20.0/24
DNS server for internal resolution 172.20.20.11
Cloudflare tunnel auth Tunnel token supplied by operator
Container image cloudflare/cloudflared:latest

Target State

Property Value
Container network Dedicated and isolated from user VLANs
Internet egress Allowed
Access to servers Explicitly allowed
Access to domain controller DNS Explicitly allowed only if needed
Default posture Final drop remains in effect for everything else

Prechecks

  • Complete P01-T09 if AD-related network policy is already in use.
  • Confirm HQ-CHR01 has working internet access.
  • Confirm the Cloudflare tunnel token or locally managed tunnel credentials are ready.
  • Confirm a persistent storage path exists for container data on CHR.
  • Confirm the RouterOS build on CHR actually supports containers before proceeding.

Current State Verification

Run on HQ-CHR01:

/system resource print
/system device-mode print
/system package print
/container/config/print
/container/print
/interface/bridge/print detail
/interface/veth/print detail
/ip firewall filter print detail where chain=forward
/ip firewall address-list print detail where list~"HQ-"

Do not continue until all of the following are true:

  • container=yes is present in /system device-mode print
  • the container package is present in /system package print
  • there is at least one writable filesystem path available for images and runtime data
  • WAN, SERVERS, and the final forward drop already exist

If container support is not available on this CHR build, stop and do not attempt to force this runbook.

Device-Mode and Package Enablement

If /system device-mode print shows container: no, enable it first:

/system/device-mode/update container=yes
/system/device-mode/print

Do not continue directly to container commands after this change. In the validated lab, the next required step was to restart HQ-CHR01 so the device-mode change was fully applied before package and container operations were attempted.

Validated sequence:

/system/device-mode/update container=yes
/system/device-mode/print
/system/shutdown

Then power the router back on from Proxmox and verify again:

/system/device-mode/print

Pass criterion for this checkpoint:

  • /system/device-mode/print shows container: yes after the restart

If /system package print does not show container, install it from the official MikroTik package URL that matches the running version.

Validated lab workflow for 7.21.5:

/tool fetch url="https://download.mikrotik.com/routeros/7.21.5/container-7.21.5.npk" dst-path=container-7.21.5.npk
/file print where name="container-7.21.5.npk"
/system reboot

After reboot, verify:

/system package print
/container print
/interface/veth print

Execution

Phase A - Create a dedicated container network

Create a bridge and veth interface:

/interface/bridge
:if ([:len [find where name="bridge-containers"]] = 0) do={
    add name=bridge-containers comment="Container bridge"
}

/interface/veth
:if ([:len [find where name="veth-cloudflared"]] = 0) do={
    add name=veth-cloudflared address=172.20.120.10/24 gateway=172.20.120.1
}

/interface/bridge/port
:if ([:len [find where bridge="bridge-containers" and interface="veth-cloudflared"]] = 0) do={
    add bridge=bridge-containers interface=veth-cloudflared
}

/ip/address
:if ([:len [find where interface="bridge-containers" and address="172.20.120.1/24"]] = 0) do={
    add address=172.20.120.1/24 interface=bridge-containers comment="Container gateway"
}

Create the interface list and membership:

/interface/list
:if ([:len [find where name="CONTAINERS"]] = 0) do={
    add name=CONTAINERS comment="Container interfaces"
}

/interface/list/member
:if ([:len [find where list="CONTAINERS" and interface="bridge-containers"]] = 0) do={
    add list=CONTAINERS interface=bridge-containers
}

Phase B - Create container-specific firewall allowances

Allow the container subnet to reach the internet:

/ip/firewall/filter
:if ([:len [find where comment="FWD - Containers to Internet"]] = 0) do={
    add chain=forward action=accept in-interface-list=CONTAINERS out-interface-list=WAN comment="FWD - Containers to Internet"
}

Allow the container subnet to reach internal servers:

/ip/firewall/filter
:if ([:len [find where comment="FWD - Containers to Servers"]] = 0) do={
    add chain=forward action=accept in-interface-list=CONTAINERS out-interface-list=SERVERS comment="FWD - Containers to Servers"
}

Allow internal DNS only if cloudflared must resolve names such as hq-fs01.corp.gntech.me:

/ip/firewall/filter
:if ([:len [find where comment="FWD - Containers to DC DNS UDP"]] = 0) do={
    add chain=forward action=accept src-address=172.20.120.0/24 dst-address=172.20.20.11 protocol=udp dst-port=53 comment="FWD - Containers to DC DNS UDP"
}
:if ([:len [find where comment="FWD - Containers to DC DNS TCP"]] = 0) do={
    add chain=forward action=accept src-address=172.20.120.0/24 dst-address=172.20.20.11 protocol=tcp dst-port=53 comment="FWD - Containers to DC DNS TCP"
}

Move the container rules above FWD - Default Drop:

{
    :local DesiredOrder {
        "FWD - Containers to Internet";
        "FWD - Containers to Servers";
        "FWD - Containers to DC DNS UDP";
        "FWD - Containers to DC DNS TCP"
    }

    :foreach RuleComment in=$DesiredOrder do={
        :foreach RuleId in=[/ip firewall filter find where comment=$RuleComment] do={
            :local DefaultDrop [/ip firewall filter find where comment="FWD - Default Drop"]
            /ip firewall filter move $RuleId $DefaultDrop
        }
    }
}

Phase C - Prepare container storage and runtime configuration

Set the temporary directory and registry URL.

Validated lab value:

/container/config
set registry-url=https://registry-1.docker.io tmpdir=containers/tmp
print

The validated CHR accepted /containers/tmp even though /file make-dir is not available in RouterOS CLI. Do not assume directory creation works like Linux shell semantics.

If the router rejects the configured path or runs out of space, stop here and add storage before continuing.

Phase D - Create the cloudflared container

Use token-based authentication for the lab.

/container
add remote-image=cloudflare/cloudflared:latest \
    interface=veth-cloudflared \
    root-dir=/containers/cf-tunnel \
    start-on-boot=yes \
    logging=yes \
    cmd="tunnel --no-autoupdate run --token <CLOUDFLARE_TUNNEL_TOKEN>"

If you accidentally create a partial or invalid container entry during testing, remove it before starting the validated one:

/container print detail
/container remove <invalid-container-id>

Phase E - Start and verify the container

/container/start [find where root-dir="/containers/cf-tunnel"]
/container/print detail
/log/print where message~"cloudflared"

Validated startup behavior included:

  • successful image download from registry-1.docker.io
  • Starting tunnel
  • Registered tunnel connection
  • precheck summary reporting healthy DNS, UDP, TCP, and Cloudflare API

The following warnings were observed and are non-blocking for the tunnel itself:

  • ICMP proxy feature is disabled
  • UDP receive buffer warning from quic-go

The tunnel still registered successfully despite those warnings.

If the token is pasted into the RouterOS CLI during testing, treat it as exposed and rotate it in Cloudflare after validation.

Validation

  • the container is present and running
  • FWD - Containers to Internet remains above FWD - Default Drop
  • FWD - Containers to Servers remains above FWD - Default Drop
  • cloudflared reaches Cloudflare successfully
  • the final intended origin server on VLAN 20 remains pending until the Cloudflare ingress configuration is corrected and then tested
  • no broad container access exists to unrelated VLANs

Validated healthy log indicators:

  • DNS Resolution ... PASS
  • UDP Connectivity ... PASS
  • TCP Connectivity ... PASS
  • Cloudflare API ... PASS
  • SUMMARY: Environment is healthy

Important: the lab token initially pulled a Cloudflare-side origin config using ssh://172.17.0.1:22, which does not match this Windows lab. After tunnel validation, correct the origin in Cloudflare Dashboard so it points to the intended internal server in 172.20.20.0/24.

Also treat any token pasted into RouterOS CLI, RouterOS logs, or this workspace as exposed. Rotate it after validation.

Evidence

  • output of /system device-mode print
  • output of /system package print
  • output of /container/print detail
  • output of /container/config/print
  • output of /interface/veth/print detail
  • output of /ip firewall filter print detail where chain=forward
  • output of /log/print where message~"cloudflared"

Rollback

  • stop and remove the container if the auth model or storage path is wrong
  • remove the CONTAINERS firewall rules if the service is abandoned
  • keep WAN, SERVERS, and the base default-drop policy intact