Using Ziti to "make Zero Trust something that is not, without touching it"

I follow all steps you suggested and it seems work, but i don't know how to ensure. Let me check if on the right way please.

Trying to understand

So, correct me if i'm wrong, i think hmisiemens cointainer actually connects to plcsiemens through the ziti-client router because in compose.yml i've not set hmisiemens as part of testnet, so "the only way to access testnet network" is through the edge-router.

A little doubt

What makes me in doubt is last hint you suggested me. Now i have PLCSIEMENS_ADDRESS=${PLCSIEMENS_ADDRESS:-172.19.1.1} and all works since i've network_mode: service:ziti-client in the compose.yml (if i remove this line, i get Unreachable peer, that's ok).
Where i should see something like PLCSIEMENS_ADDRESS="plcsiemens.ziti.internal" as you suggested?

Server-side tunneler intercepting

Last question: if i set siemensclient as part of testnet network with his own ipaddress, i can quietly connect to plcsiemens byepassing all ziti infrastructure. I thought server-side tunneler could intercept all incoming requests. Is there something else to be done to reach this goal?
When i say "make siemens client part of testnet network" i mean the following configuration:

siemensclient:
    build:
      context: ./SiemensClient
      dockerfile: Dockerfile
    stdin_open: true
    tty: true
    depends_on:
      - ${PLCSIEMENS_CONTAINER_NAME:-plcsiemens}
    environment:
      PLCSIEMENS_ADDRESS: ${PLCSIEMENS_ADDRESS:-172.19.1.1}
      PLCSIEMENS_PORT: ${PLCSIEMENS_PORT:-102}
      PLCSIEMENS_RACK: ${PLCSIEMENS_RACK:-0}
      PLCSIEMENS_SLOT: ${PLCSIEMENS_SLOT:-1}
    container_name: ${HMISIEMENS_CONTAINER_NAME:-hmisiemens}
    networks:
      testnet:
        ipv4_address: ${HMISIEMENS_ADDRESS:-172.19.1.3}
    command: ["python3", "SiemensClient.py"]

The client application container shares the network interface of the ziti-client container. This means they have the same IP address and DNS resolver.

ziti-client provides Ziti DNS to resolve intercept addresses like plcsiemens.ziti.internal to a private IP address that routes through Ziti.

1 Like

I think that's the correct value for your client application's environment, e.g. the .env file.

Ziti will grant access but does not block access in that particular way, i.e. as a firewall. You can isolate the server at the network layer then grant access with Ziti.

"Interception" in the Ziti jargon refers to a client proxy for requests that match a Ziti service address.

Ok, so the only way to reach this behaviour is isolating networks and make ziti being the only access point. There's no built-in option to do so in Ziti, right?

Yes, when using Ziti tunnelers in Docker. If a non-Docker server process listens only on the loopback address, then a tunneler on the same host can host a Ziti service with a host (target) address like 127.0.0.1:4321. The host may be attached to an untrusted network, but only authorized Ziti clients can reach the service.

You could have two Docker networks, "private" for the protected server and "public" for the client and Ziti controller/router. This would complicate the Docker compose project because you would then need to manage firewall rules, and DNS records or IPAM.

I cannot access in this way because the lib which connects to plcsiemen asks for IP address and not DNS name, so i guess i need configure the plc-client-config with the IP address (in this case 172.19.1.1)

Correct! You may use an IPv4 instead of DNS name as the intercept config address.

Last question! To reach the behaviour we were talking about before, let me know if this idea could make sense:
if i've understood correctly, all the infrastructure we built make communication evolve
from HMI <-->PLC to HMI<-->CLI_TUN<-->SRV_TUN<-->PLC.
So: does it make sense to DROP all incoming requests on PLC execpt for what came from SRV_TUN? Simply with an iptables rule!
I know that formally is horrible (network segmentation is the right way to do so), but it's good in the context of the simulation for my project!

I'm not perfectly understanding the CLI_TUN or SRV_TUN symbols, but the following is accurate and you can make ziti-host container the only access point for the PLC to simulate network isolation.

HMI<--NO_TLS-->ZITI_CLIENT<--ZITI_MTLS-->ZITI_HOST<--NO_TLS-->PLC

Yes, sorry i was referring to ZITI_HOST with SRV_TUN, my bad.
So it could be an (horrible) idea in your opinion too? I'll try and eventually refers you my results!

Thanks for all the time you wasted with me until now!

Making Ziti the only way to access your server is a good idea. Good luck!

1 Like

I've tried dropping all packets except for those which arrives from ziti-host (the server-side tunneler), but it began not working.
Basically i run the following commands:

iptables -A INPUT -s ziti-host -j ACCEPT
iptables -A INPUT -j DROP

Then i noticed that packets were arriving from ziti-client, so i changed my iptables rules making only packets from ziti-client being accetped and restart working good.
So, new iptables configuration is:

iptables -A INPUT -s ziti-client -j ACCEPT
iptables -A INPUT -j DROP

But then i start having some doubts...

tcpdumping in my plcsiemens container and sending data from siemensclient i captured this:

17:12:50.653480 IP ziti-client.testnet.34624 > c9857343ec32.iso-tsap: Flags [P.], seq 1121067225:1121067268, ack 780377771, win 251, options [nop,nop,TS val 2418989422 ecr 640141409], length 43
17:12:50.653825 IP c9857343ec32.iso-tsap > ziti-client.testnet.34624: Flags [P.], seq 1:23, ack 43, win 249, options [nop,nop,TS val 640551040 ecr 2418989422], length 22
17:12:50.653936 IP ziti-client.testnet.34624 > c9857343ec32.iso-tsap: Flags [.], ack 23, win 251, options [nop,nop,TS val 2418989423 ecr 640551040], length 0

and if i run docker compose down ziti-client (the client-side tunneler) the siemensclient container actually lost connection to the plcsiemens container.

For this reason, i guess this part of the communications is working fine!

I tried to understand why accepting only ziti-host packets plcsiemens began receiving nothing from anyone, and to do so i tried to install "things" on ziti-host container to sniff traffic and see if actually something pass through it.
Unfortunately, the image for ziti-host container is very very basic and i was able to install only iftop command and that's the output:

b652d5760133                                                        => ziti-ctrl.testnet                                                    15.8Kb  3.15Kb  3.16Kb
                                                                    <=                                                                      36.8Kb  7.36Kb  7.36Kb

Honestly i don't know exactly how iftop works, but it seems ziti-host container has only one connection with the ziti-ctrl container (ziti-controller).

Is it possible that the following part of the communication doesn't work for an unknown reason?

Just to simplify your research of the error i drop here what i think could be useful tu have.

./deviced/compose.yml

networks:
  testnet:
    name: testnet
    driver: bridge
    ipam:
      config:
        - subnet: ${NET_ID:-172.19.0.0/16}

services:

  plcsiemens:
    build:
      context: ./PLCsiemens
      dockerfile: Dockerfile
    environment:
      PLCSIEMENS_PORT: ${PLCSIEMENS_PORT:-102}
      PROCESSING_FAILURE_RATE: ${PROCESSING_FAILURE_RATE:-0.15}
      QUALITY_ASSURANCE_FAILURE_RATE: ${QUALITY_ASSURANCE_FAILURE_RATE:-0.18}
      DISCARDING_OR_SENDING_FAILURE_RATE: ${DISCARDING_OR_SENDING_FAILURE_RATE:-0.11}
      DEFECT_RATE: ${DEFECT_RATE:-0.24}
      MEMORY_AREA_SIZE: ${MEMORY_AREA_SIZE:-8}
      DATA_BLOCK_NUMBER: ${DATA_BLOCK_NUMBER:-5}
    container_name: ${PLCSIEMENS_CONTAINER_NAME:-plcsiemens}
    networks:
      testnet:
        ipv4_address: ${PLCSIEMENS_ADDRESS:-172.19.1.1}
    command: ["python3", "PLCsiemens.py"]

  siemensclient:
    build:
      context: ./SiemensClient
      dockerfile: Dockerfile
    stdin_open: true
    tty: true
    depends_on:
      - ${PLCSIEMENS_CONTAINER_NAME:-plcsiemens}
    environment:
      PLCSIEMENS_ADDRESS: ${PLCSIEMENS_ADDRESS:-172.19.1.1}
      PLCSIEMENS_PORT: ${PLCSIEMENS_PORT:-102}
      PLCSIEMENS_RACK: ${PLCSIEMENS_RACK:-0}
      PLCSIEMENS_SLOT: ${PLCSIEMENS_SLOT:-1}
    container_name: ${HMISIEMENS_CONTAINER_NAME:-hmisiemens}
    # networks:
    #  testnet:
      #  ipv4_address: ${HMISIEMENS_ADDRESS:-172.19.1.2}
    network_mode: service:ziti-client
    command: ["python3", "SiemensClient.py"]

./ziti-ctrl/compose.yml

services:
    ziti-ctrl:
        image: openziti/ziti-cli
        container_name: ziti-ctrl
        command: >
            edge quickstart
            --home /home/ziggy/quickstart
            --ctrl-address ziti-controller
            --ctrl-port 1280
            --router-address ziti-router
            --router-port 3022
            --password ziggy123
        volumes:
            - ziti-data:/home/ziggy
        networks:
            testnet:
                aliases:
                    - ziti-controller
                    - ziti-router
        expose:
            - 1280
            - 3022

    ziti-host:
        image: openziti/ziti-host:1.1.3
        container_name: ziti-host
        networks:
            testnet:
        volumes:
            -   ziti-host:/ziti-edge-tunnel
        environment:
            - ZITI_ENROLL_TOKEN=mytoken

    ziti-client:
        image: openziti/ziti-router:1.1.9
        container_name: ziti-client
        expose:
            -   3022
        networks:
            testnet:
                ipv4_address: ${HMISIEMENS_ADDRESS:-172.19.1.2}
        environment:
            ZITI_CTRL_ADVERTISED_ADDRESS: ziti-controller
            ZITI_ENROLL_TOKEN: mytoken2
            ZITI_ROUTER_NAME: tproxy
        volumes:
            -   ziti-client:/ziti-router
        dns:
            -   127.0.0.1
            -   1.1.1.1
        user: root
        cap_add:
            -   NET_ADMIN

    attacker:
        image: ubuntu:latest
        networks:
            testnet:
        profiles: ["no-auto-start"] 

volumes:
    ziti-data:
        name: ziti-data
    ziti-host:
        name: ziti-host
    ziti-client:
        name: ziti-client

plc-client-config

{
    "addresses": [
        "172.19.1.1"
    ],
    "portRanges": [
        {
            "high": 102,
            "low": 102
        }
    ],
    "protocols": [
        "tcp"
    ]
}

plc-host-config

{
    "address": "172.19.1.1",
    "port": 102,
    "protocol": "tcp"
}

Here's another way of representing the required flows. The direction of the arrow --> indicates initiating TCP to establish the flow.

  1. hmi --> ziti-client:102 (loopback TPROXY interception)
  2. ziti-client,ziti-host --> ziti-ctrl:1280,3022 (both client and host reach out to the ctrl)
  3. ziti-host --> plc:102

Ensure that only ziti-host has Ziti role attribute "plc-hosts." Then you will know the origin of the Ziti service traffic is from ziti-host --> plc:102

Here's the out of ziti edge list identities:

โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
โ”‚ ID         โ”‚ NAME              โ”‚ TYPE    โ”‚ ATTRIBUTES  โ”‚ AUTH-POLICY โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ KOUA3SD0m  โ”‚ quickstart-router โ”‚ Router  โ”‚             โ”‚ Default     โ”‚
โ”‚ UG9fOcd3C  โ”‚ plc-client        โ”‚ Default โ”‚ plc-clients โ”‚ Default     โ”‚
โ”‚ Uif9Oc63CM โ”‚ plc-host          โ”‚ Default โ”‚ plc-hosts   โ”‚ Default     โ”‚
โ”‚ V3.Kj6DVm  โ”‚ Default Admin     โ”‚ Default โ”‚             โ”‚ Default     โ”‚
โ”‚ h-jW24pbUF โ”‚ plc-client-router โ”‚ Router  โ”‚ plc-clients โ”‚ Default     โ”‚
โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ

It seems plc-host is the only one identity with "plc-hosts" role attribute.
Since we've run the command below, i guess that if i've used the right token to initialize the ziti-host container, everything should be correctly configured.

ziti edge create config "plc-host-config" host.v1 \
    '{"protocol":"tcp", "address":"plcsiemens","port":102}'

Ideas?

Alternatively: hould i try to restart all the process? Maybe i missconfigured something and we'll never know.

Ensure all containers are attached to "testnet" network in your project.

Yes, do try restarting containers in turn. Ensure ziti-client is ready before starting hmi client. In my example I added a Docker health check dependency to order the clients.

I've tried re-configuring all the infrastructur from the beginning and it still not work. It seems like the ziti-host container is completely byepassed.
Here's the output of netstat -t -u run in PLC container

Proto Recv-Q Send-Q Local Address           Foreign Address         State      
tcp        0      0 3f14bf3d52db:iso-tsap   ziti-client.testn:59262 ESTABLISHED

Isn't it supposed to be one connection to ziti-host?

The hmi client can only be configured with an IP address, not a domain name, for contacting the plc server, correct?

I think that you used the real IP address of the plc server as the Ziti intercept address.

It is possible that the real IP has a higher precedence and is bypassing the intercept.

If so, you can correct this by choosing a fictitious IP address instead of a fictitious domain name or using the real IP of the plc server.

For example, set the intercept config address like 10.11.12.13 and also configure the hmi client to connect to this IP address.

This will provide a static address and ensure the only path to the server is via Ziti if you have isolated the server at the network layer.

With fictitious IP mechanism the client cannot reach the PLC.
Here's the new configuration:

plc-host-config:

{
    "address": "172.19.1.1",
    "port": 102,
    "protocol": "tcp"
}

plc-client-config:

{
    "addresses": [
        "172.19.10.10"
    ],
    "portRanges": [
        {
            "high": 102,
            "low": 102
        }
    ],
    "protocols": [
        "tcp",
        "udp"
    ]
}

/devices/compose.yml:

networks:
  testnet:
    name: testnet
    driver: bridge
    ipam:
      config:
        - subnet: ${NET_ID:-172.19.0.0/16}

services:

  plcsiemens:
    build:
      context: ./PLCsiemens
      dockerfile: Dockerfile
    depends_on:
      - ziti-host
    environment:
      PLCSIEMENS_PORT: ${PLCSIEMENS_PORT:-102}
      PROCESSING_FAILURE_RATE: ${PROCESSING_FAILURE_RATE:-0.15}
      QUALITY_ASSURANCE_FAILURE_RATE: ${QUALITY_ASSURANCE_FAILURE_RATE:-0.18}
      DISCARDING_OR_SENDING_FAILURE_RATE: ${DISCARDING_OR_SENDING_FAILURE_RATE:-0.11}
      DEFECT_RATE: ${DEFECT_RATE:-0.24}
      MEMORY_AREA_SIZE: ${MEMORY_AREA_SIZE:-8}
      DATA_BLOCK_NUMBER: ${DATA_BLOCK_NUMBER:-5}
    container_name: ${PLCSIEMENS_CONTAINER_NAME:-plcsiemens}
    networks:
      testnet:
        ipv4_address: ${PLCSIEMENS_ADDRESS:-172.19.1.1}
    command: ["python3", "PLCsiemens.py"]

  siemensclient:
    build:
      context: ./SiemensClient
      dockerfile: Dockerfile
    stdin_open: true
    tty: true
    depends_on:
      - ${PLCSIEMENS_CONTAINER_NAME:-plcsiemens}
      - ziti-client
    environment:
      PLCSIEMENS_ADDRESS: ${PLCSIEMENS_ADDRESS:-172.19.10.10}
      PLCSIEMENS_PORT: ${PLCSIEMENS_PORT:-102}
      PLCSIEMENS_RACK: ${PLCSIEMENS_RACK:-0}
      PLCSIEMENS_SLOT: ${PLCSIEMENS_SLOT:-1}
    container_name: ${HMISIEMENS_CONTAINER_NAME:-siemensclient}
    network_mode: service:ziti-client
    command: ["python3", "SiemensClient.py"]

/ziti-ctrl/compose.yml:

services:
    ziti-ctrl:
        image: openziti/ziti-cli
        container_name: ziti-ctrl
        command: >
            edge quickstart
            --home /home/ziggy/quickstart
            --ctrl-address ziti-controller
            --ctrl-port 1280
            --router-address ziti-router
            --router-port 3022
            --password ziggy123
        volumes:
            - ziti-data:/home/ziggy
        networks:
            testnet:
                aliases:
                    - ziti-controller
                    - ziti-router
        expose:
            - 1280
            - 3022

    ziti-host:
        image: openziti/ziti-host:1.1.3
        container_name: ziti-host
        depends_on:
            - ziti-ctrl
        networks:
            testnet:
        volumes:
            -   ziti-host:/ziti-edge-tunnel
        environment:
            ZITI_ENROLL_TOKEN: tkn

    ziti-client:
        image: openziti/ziti-router:1.1.9
        container_name: ziti-client
        depends_on:
            - ziti-ctrl
        expose:
            -   3022
        networks:
            testnet:
                ipv4_address: ${HMISIEMENS_ADDRESS:-172.19.1.2}
        environment:
            ZITI_CTRL_ADVERTISED_ADDRESS: ziti-controller
            ZITI_ENROLL_TOKEN: tkn
            ZITI_ROUTER_NAME: tproxy
        volumes:
            -   ziti-client:/ziti-router
        dns:
            -   127.0.0.1
            -   1.1.1.1
        user: root
        cap_add:
            -   NET_ADMIN

volumes:
    ziti-data:
        name: ziti-data
    ziti-host:
        name: ziti-host
    ziti-client:
        name: ziti-client

EDIT: running netstat -t -u in the ziti controller both connection to ziti-host and ziti-client are established. The missing part is still the ziti-host --> plc:102 connection. The hmi --> ziti-client i guess is implicit due to the fact that both container are sort of sharing the interface thanks to network_mode: service:ziti-client in the compose.yml.

tcp        0      0 a9e5890c454a:45256      a9e5890c45:pictrography ESTABLISHED
tcp6       0      0 a9e5890c454a:csregagent ziti-host.testnet:33366 ESTABLISHED
tcp6       0      0 a9e5890c45:pictrography ziti-client.testn:44184 ESTABLISHED
tcp6       0      0 a9e5890c45:pictrography a9e5890c454a:45256      ESTABLISHED
tcp6       0      0 a9e5890c454a:csregagent ziti-client.testn:50040 ESTABLISHED
tcp6       0      0 a9e5890c454a:csregagent ziti-client.testn:50030 ESTABLISHED

EDIT2: Should we change strategy? Maybe not using quickstart containers, but some other which requires extra explicit configuration.

EDIT3: Some of ziti-client's log are the following

ziti-client  | {"file":"github.com/openziti/ziti/router/state/apiSessionAdded.go:124","func":"github.com/openziti/ziti/router/state.(*apiSessionAddedHandler).applySync","level":"info","msg":"finished synchronizing api sessions [count: 10, syncId: cm0l6zjuj003101lbtdsv6j0b, duration: 127.195ยตs]","time":"2024-09-02T16:05:37.191Z"}
ziti-client  | {"file":"github.com/openziti/ziti/tunnel/dns/server.go:119","func":"github.com/openziti/ziti/tunnel/dns.NewDnsServer","level":"info","msg":"dns server running at 127.0.0.1:53","time":"2024-09-02T16:05:38.184Z"}
ziti-client  | {"file":"github.com/openziti/ziti/tunnel/dns/server.go:271","func":"github.com/openziti/ziti/tunnel/dns.(*resolver).AddHostname","level":"info","msg":"adding ziti-tunnel.resolver.test = 19.65.28.94 to resolver","time":"2024-09-02T16:05:38.184Z"}
ziti-client  | {"file":"github.com/openziti/ziti/tunnel/dns/server.go:300","func":"github.com/openziti/ziti/tunnel/dns.(*resolver).RemoveHostname","level":"info","msg":"removing ziti-tunnel.resolver.test from resolver","time":"2024-09-02T16:05:38.229Z"}
ziti-client  | {"file":"github.com/openziti/ziti/tunnel/intercept/iputils.go:51","func":"github.com/openziti/ziti/tunnel/intercept.SetDnsInterceptIpRange","level":"info","msg":"dns intercept IP range: 100.64.0.1 - 100.127.255.255","time":"2024-09-02T16:05:38.229Z"}
ziti-client  | {"file":"github.com/openziti/ziti/tunnel/intercept/svcpoll.go:155","func":"github.com/openziti/ziti/tunnel/intercept.(*ServiceListener).HandleServicesChange","level":"info","msg":"adding service","service":"plc-siemens-service","time":"2024-09-02T16:05:38.234Z"}
ziti-client  | {"file":"github.com/openziti/ziti/tunnel/intercept/svcpoll.go:226","func":"github.com/openziti/ziti/tunnel/intercept.(*ServiceListener).addService","level":"info","msg":"starting tunnel for newly available service plc-siemens-service","serviceId":"WLtDWbS6auakI1TitWeKF","serviceName":"plc-siemens-service","time":"2024-09-02T16:05:38.235Z"}
ziti-client  | {"file":"github.com/openziti/ziti/tunnel/intercept/svcpoll.go:228","func":"github.com/openziti/ziti/tunnel/intercept.(*ServiceListener).addService","level":"error","msg":"failed to intercept service: can not intercept services in host mode","serviceId":"WLtDWbS6auakI1TitWeKF","serviceName":"plc-siemens-service","time":"2024-09-02T16:05:38.235Z"}

I am mobile and will offer a quick answer with more detailed follow up on Tuesday or Thursday this week.

My hypothesis was the intercept had a lower precedence than the real IP because the real address'es network is directly attached.

To test this, I suggest a fictitious intercept (meaning from a subnet that is not attached to the client container).

I take it you tried inventing an intercept IP address in the same subnet that is attached to the client, but that would suffer from the same problem if the hypothesis is correct.

Assuming there are no networks in 10.0.0.0/8 attached to the client you can safely us an intercept address like 10.11.12.13.

1 Like