Cette page existe aussi en français
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 one you open an SSH session into. That is what separates an emulator from a mock server: the block a client asks for is the block it gets, 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 learn
- Choose the machine mode according to what you want to prove.
- Install Incus and OVN on Debian 13, OVN wiring included.
- Verify 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 network bridge, carrying the address the API published.
The four modes do not deliver the same thing, and one difference really counts:
--vm | Machines | Addresses | Firewall | Cross-VPC isolation | Own kernel | Requires |
|---|---|---|---|---|---|---|
off (default) | no | no | no | no | no | nothing |
incus | yes | yes | yes | no | no | Incus 6.0.4+ |
incus-vm | yes | yes | yes | no | yes | Incus + KVM |
incus-ovn | yes | yes | yes | yes | no | Incus + OVN |
Isolation is the only capability whose verdict changes with the mode. Two private networks of two different VPCs must not reach each other. In bridge mode they do, and the runtime documents the cause itself: traffic between managed bridges on one host is routed directly. An OVN network is a logical network with its own router, so the separation comes from topology rather than from a rule.
Rather than guessing, ask the emulator what its mode can prove:
feint start --vm incuscurl -s localhost:4599/_feint/health | jq -c '.capabilities'{"machines":true,"addresses":true,"firewall":true,"isolation":false,"own_kernel":false,"private_from_host":true}Installing 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 the repository choice, because it is arguable. Debian 13 "trixie" packages Incus 6.0.4, exactly the floor below which network ACLs 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 the Incus maintainer, provides the recommended 7.2 series, the one the project's measurements were taken on.
-
Install the prerequisites and fetch the repository key into its own keyring. A key placed in
/etc/apt/keyringssigns only the repository naming it, where the system trust store would vouch for everything.Fenêtre de terminal apt-get updateapt-get install -y --no-install-recommends ca-certificates curl gpg uidmapmkdir -p /etc/apt/keyringscurl -fsSL https://pkgs.zabbly.com/key.asc -o /etc/apt/keyrings/zabbly.ascgpg --show-keys --with-fingerprint /etc/apt/keyrings/zabbly.ascCheck the fingerprint before going further: it must be
4EFC 5906 96CB 15B8 7C73 A3AD 82CC 8797 C838 DCFD, forZabbly Kernel Builds. That is the only moment you decide whom to trust. -
Declare the repository, deriving the suite and architecture from the machine rather than hardcoding them.
Fenêtre de terminal cat > /etc/apt/sources.list.d/zabbly-incus.sources <<EOFEnabled: yesTypes: debURIs: https://pkgs.zabbly.com/incus/stableSuites: $(. /etc/os-release && echo "$VERSION_CODENAME")Components: mainArchitectures: $(dpkg --print-architecture)Signed-By: /etc/apt/keyrings/zabbly.ascEOFOn Debian 13,
Suitesistrixie. There is nocurl | shhere: every file is written to disk, and the key is verified before a single package arrives. -
Install Incus, OVN and Open vSwitch. Incus comes from Zabbly, the OVN packages from Debian.
Fenêtre de terminal apt-get updateapt-get install -y --no-install-recommends \incus incus-client ovn-central ovn-host openvswitch-switchImmediate check:
incus --versionmust answer7.2andovn-nbctl --versionovn-nbctl 25.03.0on an up-to-date trixie. -
Start the services, then wait. The systemd units return before the OVN databases are listening, and a command issued in that window fails for no apparent reason.
Fenêtre de terminal systemctl enable --now openvswitch-switch ovn-central ovn-hostwhile [ ! -S /run/ovn/ovnnb_db.sock ]; do sleep 1; done -
Tell Open vSwitch where the southbound database is. This is half of the wiring, the half linking 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 -
Initialise Incus, then give the uplink bridge the range OVN networks will carve out of it.
Fenêtre de terminal incus admin init --minimalincus network set incusbr0 ipv4.address=10.108.0.1/24incus network set incusbr0 \ipv4.dhcp.ranges=10.108.0.10-10.108.0.99 \ipv4.ovn.ranges=10.108.0.100-10.108.0.199The address is set in a separate command from the ranges: those are validated against the bridge's current address, and
incus admin init --minimalpicks one at random. Withoutipv4.ovn.ranges, creating an OVN network is refused with a message blaming the network rather than the bridge. -
Verify with a real network, not with the list of installed packages.
Fenêtre de terminal incus network create ovntest --type=ovn network=incusbr0incus network delete ovntestNetwork ovntest createdvalidates the whole wiring. It is the only check that proves anything: packages installed but wired wrong pass every other one.
Do not disable AppArmor. The temptation is real, because the OVN project's own CI turns AppArmor off before its system tests. That workaround targets binaries built from source, outside the paths packaged profiles cover. A package install ships the profiles it needs: on the test VM, Incus, OVN and Open vSwitch all work with AppArmor active.
The final verdict is not read from apt, but from the declared capability:
feint doctor --vm autofeint start --vm autocurl -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 three commands verify it. 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.
# what the API publishescurl -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 runtimeincus exec "feint-scw-<server-id>" -- ip -4 -o addr showOn my machine, IPAM publishes 10.51.0.2/24 and the container carries
10.51.0.2/24: both addresses match. The same run under
--vm incus-ovn gives 10.52.0.2/24 on both sides.
Everything the emulator creates is labelled, and feint clean removes
exactly those objects, touching nothing else on your host:
removed 1 machine(s), 1 network(s), 1 rule set(s)Opening 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 covered the private plane, the subnet address; this one covers the public plane, the one you log in through. This is where emulation stops being a matter of JSON responses: you register a key through the provider's API, create a server, and open a session with the ordinary SSH client, exactly as on the real cloud.

Each provider publishes its addresses in its own documentation block, and
that detail has a precise reason: two emulated clouds on the same machine must
never route the same /32.
| Provider | Published block | Standard | Default user |
|---|---|---|---|
| Scaleway | 203.0.113.0/24 | TEST-NET-3 | root |
| Outscale | 198.51.100.0/24 | TEST-NET-2 | outscale |
| Exoscale | 192.0.2.0/24 | TEST-NET-1 | whatever the template declares |
These ranges are reserved for documentation by RFC 5737: they are
routable nowhere on the Internet, which makes them exactly right for a
public address that exists only on your machine. The path below needs an
active machine runtime, so --vm incus at minimum.
ssh-keygen -t ed25519 -N '' -C feint-lab -f ./ideval "$(feint env scaleway)"
# the key goes through the IAM API, as on the real Scalewayscw iam ssh-key create name=feint-lab public-key="$(cat ./id.pub)"
# a flexible IP, then the server carrying itip_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 log in toip=$(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"Inside the machine, the interface carries the published address as a /32,
beside the network's private address:
ssh-labrooteth0 203.0.113.2/32eth0 10.209.84.9/24eval "$(feint env outscale)"
oapi-cli CreateKeypair --KeypairName feint-lab --PublicKey "$(cat ./id.pub)"ip_json=$(oapi-cli CreatePublicIp)ip_id=$(echo "$ip_json" | jq -r '.PublicIp.PublicIpId')public=$(echo "$ip_json" | jq -r '.PublicIp.PublicIp')
vm=$(oapi-cli CreateVms --ImageId ami-00000001 --VmType tinav6.c1r1p2 \ --KeypairName feint-lab | jq -r '.Vms[0].VmId')oapi-cli LinkPublicIp --PublicIpId "$ip_id" --VmId "$vm"
ssh -F /dev/null -i ./id -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "outscale@$public"The fingerprint returned by CreateKeypair is the one ssh-keygen
computes, and ReadVms republishes the same address CreatePublicIp
returned.
i-577826f8outscaleThe user is not a property of the cloud but of the template, which declares
it in its default-user field. Reading it rather than assuming is the
only correct approach.
eval "$(feint env exoscale)"
exo compute ssh-key register feint-lab ./id.pubtpl=$(exo -O json compute instance-template list | jq -r '.[0].name')type=$(exo -O json compute instance-type list | jq -r '"\(.[0].family).\(.[0].name)"')
exo compute instance create ssh-lab --zone "$EXOSCALE_ZONE" \ --template "$tpl" --instance-type "$type" --ssh-key feint-lab
# the address you log in to is an attached elastic IPinst=$(exo -O json compute instance list | jq -r '.[] | select(.name=="ssh-lab") | .id')exo -O json compute elastic-ip create --description feint-labeip=$(exo -O json compute elastic-ip list | jq -r '.[0].ip_address')exo compute instance elastic-ip attach "$inst" "$eip"
ssh -F /dev/null -i ./id -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "debian@$eip"On the emulated catalogue's Debian 12 template, default-user is debian,
and the elastic IP lands on 192.0.2.1.
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.
Under --vm incus-ovn, the health call returns "isolation":true and
"private_from_host":false. The two capabilities therefore move 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.
Key points
- By default feint runs no machine:
offis a control plane, and that is what CI runs. - Only
--vm incus-ovnisolates two VPCs, at the cost of a private address the host can no longer reach. - 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 cleanremoves only what the emulator created, the rest of the host is never touched.