Skip to content
Français
Cloud medium

Run real machines and connect to them over SSH

2 min de lecture

Read this page in French

Give feint a runtime and a server created through the API becomes a real machine, on a real bridge, carrying the address the API published, and you open an SSH session inside it. That is what separates an emulator from a mock server: the block you asked for is the block you get, a security group really closes a port, and the machine answers. This page compares the four modes, gives the Incus and OVN on Debian 13 procedure, and walks the SSH path on all three providers.

What you will get

By the end of this page, a machine created through the API will really run on your host, will carry the address the API published, and you will open an SSH session into it on all three providers.

  • Choose the machine mode according to what you want to prove.
  • Install Incus and OVN on Debian 13, OVN wiring included.
  • Check that the machine carries the address the API published.
  • Open an SSH session on emulated Scaleway, Outscale and Exoscale.

Real machines, on request

By default feint is a control plane: it answers, and nothing runs. Give it a runtime and a created server becomes a real container or virtual machine, on a real bridge, carrying the address the API published. That is the difference between an emulator and a plain mock server.

The four modes do not deliver the same thing, and only one difference really matters:

--vmMachinesAddressesFirewallInter-VPC isolationOwn kernelRequires
off (default)nonononononothing
incusyesyesyesnonoIncus 6.0.4+
incus-vmyesyesyesnoyesIncus + KVM
incus-ovnyesyesyesyesnoIncus + OVN

Isolation is the only capability whose verdict changes with the mode. Two private networks belonging to two different VPCs must not reach each other. In bridge mode they do, and the cause is documented by the runtime itself: traffic between managed bridges on the same host is routed directly. An OVN network is a logical network with its own router, so the separation comes from the topology rather than from a rule.

Rather than guessing, ask the emulator what its mode can prove:

Fenêtre de terminal
feint start --vm incus
curl -s localhost:4599/_feint/health | jq -c '.capabilities'
{"machines":true,"addresses":true,"firewall":true,"isolation":false,"own_kernel":false}

The same call in --vm incus-ovn returns "isolation":true. That declaration is meant to be read by your tests: a suite comparing a hardcoded mode name has to be edited on every change, a suite reading the capabilities keeps working.

How to install Incus and OVN on Debian 13

Debian 13 is the simplest distribution for this mode, and the procedure has three parts: the repository, the packages, the OVN wiring. The last one is what nobody guesses, and without it incus network create --type=ovn fails with an error naming neither the cause nor the missing setting.

A word on choosing the repository, because it is debatable. Debian 13 "trixie" packages Incus 6.0.4, exactly the floor below which access control lists on an interface are refused. The distribution package is therefore enough, unlike Ubuntu 24.04, which ships 6.0.0 and will not move. The Zabbly repository, published by Incus' maintainer, brings the recommended 7.2 series, the one the project's measurements were made on. On my Debian 13 VM, apt-cache policy incus does print 6.0.4-2+deb13u8 before the repository is added.

  1. Install the prerequisites and fetch the repository key into its own keyring. A key placed in /etc/apt/keyrings signs only the repository that names it, where the system store would count for everything.

    Fenêtre de terminal
    apt-get update
    apt-get install -y --no-install-recommends ca-certificates curl gpg uidmap
    mkdir -p /etc/apt/keyrings
    curl -fsSL https://pkgs.zabbly.com/key.asc -o /etc/apt/keyrings/zabbly.asc
    gpg --show-keys --with-fingerprint /etc/apt/keyrings/zabbly.asc

    Check the fingerprint printed before going further: it must be 4EFC 5906 96CB 15B8 7C73 A3AD 82CC 8797 C838 DCFD, for Zabbly Kernel Builds. That is the one moment where you decide whom you trust.

  2. Declare the repository, deriving the suite and the architecture from the machine rather than hardcoding them.

    Fenêtre de terminal
    cat > /etc/apt/sources.list.d/zabbly-incus.sources <<EOF
    Enabled: yes
    Types: deb
    URIs: https://pkgs.zabbly.com/incus/stable
    Suites: $(. /etc/os-release && echo "$VERSION_CODENAME")
    Components: main
    Architectures: $(dpkg --print-architecture)
    Signed-By: /etc/apt/keyrings/zabbly.asc
    EOF

    On Debian 13, Suites is trixie. No curl | sh in this procedure: every file is written to disk, and the key is verified before a single package arrives.

  3. Install Incus, OVN and Open vSwitch. Incus comes from Zabbly, the OVN packages from Debian.

    Fenêtre de terminal
    apt-get update
    apt-get install -y --no-install-recommends \
    incus incus-client ovn-central ovn-host openvswitch-switch

    Immediate check: incus --version must answer 7.2 and ovn-nbctl --version ovn-nbctl 25.03.0 on an up-to-date trixie.

  4. Start the services, then wait. The systemd units return before the OVN databases are listening, and a command run in that window fails for no apparent reason.

    Fenêtre de terminal
    systemctl enable --now openvswitch-switch ovn-central ovn-host
    while [ ! -S /run/ovn/ovnnb_db.sock ]; do sleep 1; done
  5. Tell Open vSwitch where the southbound database is. That is half the wiring, the half connecting the switch to the OVN control plane.

    Fenêtre de terminal
    ovs-vsctl set open_vswitch . \
    external_ids:ovn-remote=unix:/run/ovn/ovnsb_db.sock \
    external_ids:ovn-encap-type=geneve \
    external_ids:ovn-encap-ip=127.0.0.1
  6. Initialise Incus, then give the uplink bridge the range the OVN networks will carve out of it.

    Fenêtre de terminal
    incus admin init --minimal
    incus network set incusbr0 ipv4.address=10.108.0.1/24
    incus network set incusbr0 \
    ipv4.dhcp.ranges=10.108.0.10-10.108.0.99 \
    ipv4.ovn.ranges=10.108.0.100-10.108.0.199

    The address is set in a separate command from the ranges: those are validated against the bridge's current address, and incus admin init --minimal picks one at random. Without ipv4.ovn.ranges, creating an OVN network is refused with a message blaming the network rather than the bridge.

  7. Verify with a real network, not with the list of installed packages.

    Fenêtre de terminal
    incus network create ovntest --type=ovn network=incusbr0
    incus network delete ovntest

    Network ovntest created validates the whole wiring. It is the only check that proves anything: packages installed but badly wired pass every other one.

Do not disable AppArmor. The temptation exists, because the OVN project's own continuous integration turns AppArmor off before its system tests. That workaround targets binaries built from source, outside the paths the packaged profiles cover. A package installation ships the profiles it needs: on the test VM, Incus, OVN and Open vSwitch all run with AppArmor active. Turning off a mandatory access control to run a testing tool is a bad trade.

The final verdict is not read in apt, but in the declared capability:

Fenêtre de terminal
feint doctor --vm auto
feint start --vm auto
curl -s localhost:4599/_feint/health | jq -c '.machines, .capabilities.isolation'

On a freshly installed Debian 13 VM, doctor reports machine runtime: incus-ovn with isolation true, and the health API confirms "incus-ovn" then true. A false value means OVN is absent, or present but not wired.

Does the machine really carry the published address?

Yes, and it takes three commands to check. Create a private network on a chosen block, attach a server to it, start it, then ask the runtime, not the emulator, what the machine carries.

Fenêtre de terminal
# the block you asked for is the block you get
curl -s -X POST "http://127.0.0.1:4599/vpc/v2/regions/fr-par/private-networks" \
-H 'Content-Type: application/json' \
-d '{"name":"lab","subnets":["10.51.0.0/24"]}' | jq -c '{id, subnet: .subnets[0].subnet}'
# what the API publishes
curl -s "http://127.0.0.1:4599/ipam/v1/regions/fr-par/ips" | jq -r '.ips[0].address'
# what the machine carries, seen from the runtime
incus exec "feint-scw-<server-id>" -- ip -4 -o addr show

On my workstation, IPAM publishes 10.51.0.2/24 and the container carries 10.51.0.2/24: the two addresses match. The same attempt in --vm incus-ovn gives 10.52.0.2/24 on both sides. The machine created is called feint-scw-<server identifier>, which makes it easy to find.

Everything the emulator creates is labelled, and feint clean removes exactly those objects, without touching the rest of your host:

Fenêtre de terminal
feint clean --vm incus
removed 1 machine(s), 1 network(s), 1 rule set(s)

If Incus is not installed on your machine yet, the site's Incus section covers the setup.

How to open an SSH session on an emulated machine

The public address the API publishes is really routed, and you can SSH into it on all three providers. The previous section was about the private plane, the subnet's address; this one is about the public plane, the one you open a session through. This is where emulation stops being a matter of JSON answers: you register a key through the provider's API, you create a server, and you open a session on it with the ordinary SSH client, exactly as on the real cloud.

SSH session open on a machine created through the emulated Scaleway API, at the published address 203.0.113.2

Each provider publishes its addresses in its own documentation block, and that detail has a precise reason: two clouds emulated on the same machine must never route the same /32.

ProviderPublished blockStandardDefault user
Scaleway203.0.113.0/28TEST-NET-3root
Outscale198.51.100.0/28TEST-NET-2outscale
Exoscale192.0.2.0/28TEST-NET-1whatever the template declares

Those ranges are reserved for documentation by RFC 5737: they are routable nowhere on the Internet, which makes them exactly the right choice for a public address existing only on your machine. The path below needs an active machine runtime, so --vm incus at minimum.

Fenêtre de terminal
ssh-keygen -t ed25519 -N '' -C feint-lab -f ./id
eval "$(feint env scaleway)"
# the key goes through the IAM API, as on the real Scaleway
scw iam ssh-key create name=feint-lab public-key="$(cat ./id.pub)"
# a flexible address, then the server carrying it
ip_id=$(scw instance ip create zone=fr-par-1 -o json | jq -r '(.ip // .).id')
srv=$(scw instance server create name=ssh-lab type=DEV1-S zone=fr-par-1 ip="$ip_id" -o json | jq -r .id)
scw instance server start "$srv" zone=fr-par-1
# the address the API publishes is the one you connect to
ip=$(scw instance server get "$srv" zone=fr-par-1 -o json | jq -r '.public_ips[0].address')
ssh -F /dev/null -i ./id -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "root@$ip"

Once inside the machine, the interface carries the published address, and it alone:

ssh-lab
root
eth0 203.0.113.2/32

One address only, as on the real cloud. Earlier versions carried two: the upstream images contain no SSH daemon, measured on the four public images tested, so the machine installed one on first boot, which required Internet egress, therefore address translation, therefore a second network. The emulator now builds its own images with the daemon already present, and that second address is gone. A real Scaleway account and a real Exoscale account were measured to settle it: one address each, never two.

Fenêtre de terminal
feint images --check
ok almalinux/9 9d351a392b89
ok alpine/3.21 b2f8e0301664
ok debian/12 7bf813f9e3ea
ok ubuntu/22.04 41015360bee3
ok ubuntu/24.04 3684ba893a51
every machine image is present

feint images builds the five images, --check reports what is missing and exits 2, and feint doctor names them without ever building anything: a build starts a container and takes several minutes, which this tool prefers to ask for rather than trigger.

What the host can reach depends on the mode

A private subnet address answers from the host in bridge mode, but not under OVN, and the emulator declares it rather than leaving you to guess. The cause is topological: the logical router separating two VPCs also translates connections coming from the host on the way back.

Fenêtre de terminal
curl -s localhost:4599/_feint/health | jq -c '.capabilities'
{"machines":true,"addresses":true,"firewall":true,"isolation":false,"own_kernel":false,"private_from_host":true}

In --vm incus-ovn, the same request returns "isolation":true and "private_from_host":false. The two capabilities therefore vary in opposite directions, and that is the trade-off to know: the mode that truly isolates two VPCs is the one from which your workstation cannot reach an internal address. The public plane stays reachable in both modes, which is precisely why the SSH paths above go through a public address rather than a private one.

Key takeaways

  • By default feint runs no machines: off mode is a control plane, and that is what CI runs.
  • Only --vm incus-ovn isolates two VPCs, at the cost of a private address the host no longer reaches.
  • The machine carries the published address, verifiable from the runtime rather than from the emulator.
  • An SSH session opens on all three providers, each on its own documentation address block.
  • feint clean removes only what the emulator created, the rest of the host is never touched.

Next steps

Is this site useful to you?

Fewer than 1% of readers support this site.

I maintain more than 700 free guides, with no ads and no tracking. Any support, even a symbolic one, helps cover hosting and keeps these resources free. Thank you for the help.

The form does not show? Open Ko-fi in a new tab.

Subscribe and follow my DevSecOps work on LinkedIn