iSCSI carries SCSI commands over TCP, so a server can use a disk that physically lives on another machine as if it were a local block device. In this tutorial you will turn one Ubuntu 24.04 server into an iSCSI target with the kernel's LIO subsystem and targetcli, restrict access with an ACL and CHAP authentication, and connect a second server as initiator that formats, mounts and reconnects to the disk automatically at boot.
Prerequisites
To follow this guide you need:
- Two servers running Ubuntu 24.04 LTS, for example two CubePath VPS instances, each with a non-root user that has
sudoprivileges:- The target (storage server), private IP
10.0.0.10in the examples. - The initiator (client), private IP
10.0.0.20in the examples.
- The target (storage server), private IP
- On the target, either a spare empty disk (this guide uses
/dev/vdb) or enough free space for a disk image file. - A private network between both servers. iSCSI data is not encrypted, so never expose port 3260 to the Internet.
Replace the IPs with your own throughout the guide.
Understanding the iSCSI terms
A few names appear in every command:
| Term | Meaning |
|---|---|
| Target | The server that exports storage |
| Initiator | The client that consumes it |
| IQN | A unique name such as iqn.2026-09.com.example:storage.disk1, in the form iqn.<year-month>.<reversed domain>:<identifier> |
| Backstore | The real storage behind the export: a block device or a file |
| LUN | A backstore exposed through a target, seen by the initiator as a disk |
| TPG | Target portal group: holds the LUNs, ACLs and listening addresses (portals) of a target |
| ACL | The list of initiator IQNs allowed to log in |
WarningA plain iSCSI LUN must be mounted by only one initiator at a time. Ext4 and XFS are not cluster filesystems, and two hosts writing to the same LUN will corrupt it.
Step 1 - Installing targetcli on the target
On the target, install targetcli-fb, the management shell for the in-kernel LIO target:
sudo apt update
sudo apt install targetcli-fb
The configuration you build is saved to /etc/rtslib-fb-target/saveconfig.json and restored at boot by the rtslib-fb-targetctl service. Make sure the service is enabled:
sudo systemctl enable rtslib-fb-targetctl
Open the shell once to confirm it works, and list the empty configuration tree:
sudo targetcli ls
o- / ..................................................................... [...]
o- backstores .......................................................... [...]
| o- block .............................................. [Storage Objects: 0]
| o- fileio ............................................. [Storage Objects: 0]
| o- pscsi .............................................. [Storage Objects: 0]
| o- ramdisk ............................................ [Storage Objects: 0]
o- iscsi ........................................................ [Targets: 0]
o- loopback ..................................................... [Targets: 0]
o- vhost ........................................................ [Targets: 0]
Every targetcli command in this guide can also be typed inside the interactive shell (sudo targetcli). Passing them as arguments makes them easy to copy and script.
Step 2 - Creating the backstore
The backstore is what the initiator will write to. A whole block device gives the best performance. Confirm the disk is empty:
lsblk -o NAME,SIZE,TYPE,MOUNTPOINTS /dev/vdb
NAME SIZE TYPE MOUNTPOINTS
vdb 100G disk
Create a block backstore named disk1 on it:
sudo targetcli /backstores/block create name=disk1 dev=/dev/vdb
Created block storage object disk1 using /dev/vdb.
If you do not have a spare disk, use a file instead. This creates a 20 GB sparse image under /srv/iscsi:
sudo mkdir -p /srv/iscsi
sudo targetcli /backstores/fileio create name=disk1 file_or_dev=/srv/iscsi/disk1.img size=20G
Use one or the other, not both, since both use the name disk1. The rest of the guide refers to /backstores/block/disk1; change it to /backstores/fileio/disk1 if you created a file.
Step 3 - Creating the target and LUN
Create the target with your IQN. targetcli automatically creates tpg1 and a portal listening on all addresses:
sudo targetcli /iscsi create iqn.2026-09.com.example:storage.disk1
Created target iqn.2026-09.com.example:storage.disk1.
Created TPG 1.
Global pref auto_add_default_portal=true
Created default portal listening on all IPs (0.0.0.0), port 3260.
Replace the default portal with one bound to the private IP only, so the target does not listen on the public interface:
sudo targetcli /iscsi/iqn.2026-09.com.example:storage.disk1/tpg1/portals delete 0.0.0.0 3260
sudo targetcli /iscsi/iqn.2026-09.com.example:storage.disk1/tpg1/portals create 10.0.0.10 3260
Export the backstore as LUN 0:
sudo targetcli /iscsi/iqn.2026-09.com.example:storage.disk1/tpg1/luns create /backstores/block/disk1
Created LUN 0.
Step 4 - Restricting access with an ACL and CHAP
Right now no initiator is allowed in, because the TPG only accepts initiators listed in its ACLs. You will allow the client by its IQN and require a CHAP username and password.
First, decide the client's IQN. Ubuntu generates one at install time; read it on the initiator:
sudo apt update
sudo apt install open-iscsi
sudo cat /etc/iscsi/initiatorname.iscsi
## DO NOT EDIT OR REMOVE THIS FILE!
## If you remove this file, the iSCSI daemon will not start.
## If you change the InitiatorName, existing access control lists
## may reject this initiator. The InitiatorName must be unique
## for each iSCSI initiator. Do NOT duplicate iSCSI InitiatorNames.
InitiatorName=iqn.2004-10.com.ubuntu:01:3c9d2e7f1a4b
A readable name makes ACLs easier to manage. Edit the file and set your own:
sudo nano /etc/iscsi/initiatorname.iscsi
InitiatorName=iqn.2026-09.com.example:client1
Restart the daemon so it picks up the new name:
sudo systemctl restart iscsid
Back on the target, create the ACL for that IQN. The LUN is mapped to it automatically:
sudo targetcli /iscsi/iqn.2026-09.com.example:storage.disk1/tpg1/acls create iqn.2026-09.com.example:client1
Created Node ACL for iqn.2026-09.com.example:client1
Created mapped LUN 0.
Generate a CHAP password. Some initiators, Windows in particular, only accept CHAP secrets of 12 to 16 characters, so this command creates exactly 16 random characters:
openssl rand -hex 8
Set the CHAP credentials on the ACL, replacing your_chap_password with the generated value, and turn authentication on for the TPG:
sudo targetcli /iscsi/iqn.2026-09.com.example:storage.disk1/tpg1/acls/iqn.2026-09.com.example:client1 set auth userid=client1 password=your_chap_password
sudo targetcli /iscsi/iqn.2026-09.com.example:storage.disk1/tpg1 set attribute authentication=1
Save the configuration so it survives a reboot:
sudo targetcli saveconfig
Configuration saved to /etc/rtslib-fb-target/saveconfig.json
Review the full tree:
sudo targetcli ls /iscsi
o- iscsi .......................................................... [Targets: 1]
o- iqn.2026-09.com.example:storage.disk1 ............................ [TPGs: 1]
o- tpg1 .......................................... [no-gen-acls, auth per-acl]
o- acls ........................................................ [ACLs: 1]
| o- iqn.2026-09.com.example:client1 ...................... [Mapped LUNs: 1]
| o- mapped_lun0 ................................ [lun0 block/disk1 (rw)]
o- luns ........................................................ [LUNs: 1]
| o- lun0 .............................. [block/disk1 (/dev/vdb) (default)]
o- portals .................................................. [Portals: 1]
o- 10.0.0.10:3260 ................................................. [OK]
The exact flags in brackets next to tpg1 vary by version. What matters is one ACL with one mapped LUN and one portal on the private IP.
Step 5 - Opening the firewall on the target
Allow iSCSI only from the initiator's private IP:
sudo ufw allow from 10.0.0.20 to any port 3260 proto tcp
sudo ufw status
Status: active
To Action From
-- ------ ----
OpenSSH ALLOW Anywhere
3260/tcp ALLOW 10.0.0.20
If UFW is inactive, run sudo ufw allow OpenSSH and sudo ufw enable first.
Step 6 - Discovering and logging in from the initiator
On the initiator, ask the target which targets it offers. Discovery does not require CHAP in this setup:
sudo iscsiadm -m discovery -t sendtargets -p 10.0.0.10
10.0.0.10:3260,1 iqn.2026-09.com.example:storage.disk1
Discovery creates a node record for the target. Store the CHAP credentials in it, replacing your_chap_password with the value from Step 4:
TARGET=iqn.2026-09.com.example:storage.disk1
sudo iscsiadm -m node -T "$TARGET" -p 10.0.0.10 --op update -n node.session.auth.authmethod -v CHAP
sudo iscsiadm -m node -T "$TARGET" -p 10.0.0.10 --op update -n node.session.auth.username -v client1
sudo iscsiadm -m node -T "$TARGET" -p 10.0.0.10 --op update -n node.session.auth.password -v your_chap_password
Tell the initiator to log in to this target automatically at boot:
sudo iscsiadm -m node -T "$TARGET" -p 10.0.0.10 --op update -n node.startup -v automatic
Log in now:
sudo iscsiadm -m node -T "$TARGET" -p 10.0.0.10 --login
Logging in to [iface: default, target: iqn.2026-09.com.example:storage.disk1, portal: 10.0.0.10,3260]
Login to [iface: default, target: iqn.2026-09.com.example:storage.disk1, portal: 10.0.0.10,3260] successful.
Check the session and find the name of the new disk:
sudo iscsiadm -m session -P 3 | grep -E "Target:|Attached scsi disk"
Target: iqn.2026-09.com.example:storage.disk1 (non-flash)
Attached scsi disk sda State: running
lsblk now shows sda with the size of the backstore:
lsblk -o NAME,SIZE,TYPE,TRAN /dev/sda
NAME SIZE TYPE TRAN
sda 100G disk iscsi
Step 7 - Formatting and mounting the LUN
The LUN behaves like any empty disk. Create a filesystem on it. This only needs to be done once:
sudo mkfs.ext4 -L iscsidata /dev/sda
Device names like sda can change between boots, so mount by UUID. Read it:
sudo blkid -s UUID -o value /dev/sda
6b1f2c8e-4d7a-4e0b-9a51-2f3c7d8e9b10
Create the mount point and add an entry to /etc/fstab:
sudo mkdir -p /mnt/iscsi
sudo nano /etc/fstab
UUID=6b1f2c8e-4d7a-4e0b-9a51-2f3c7d8e9b10 /mnt/iscsi ext4 defaults,_netdev,nofail 0 2
Use your own UUID. _netdev makes systemd wait for the network and the iSCSI login before mounting, and nofail lets the server boot even if the target is unreachable.
Mount it and test a write:
sudo systemctl daemon-reload
sudo mount -a
echo "hello from iscsi" | sudo tee /mnt/iscsi/test.txt
df -h /mnt/iscsi
hello from iscsi
Filesystem Size Used Avail Use% Mounted on
/dev/sda 98G 24K 93G 1% /mnt/iscsi
Step 8 - Verifying the setup survives a reboot
The open-iscsi service logs in to every node marked automatic at boot. Make sure it is enabled, then reboot the initiator:
sudo systemctl enable open-iscsi
sudo reboot
After it comes back, check the session and the mount:
sudo iscsiadm -m session
findmnt /mnt/iscsi
cat /mnt/iscsi/test.txt
tcp: [1] 10.0.0.10:3260,1 iqn.2026-09.com.example:storage.disk1 (non-flash)
TARGET SOURCE FSTYPE OPTIONS
/mnt/iscsi /dev/sda ext4 rw,relatime
hello from iscsi
To disconnect cleanly (for example before maintenance on the target), unmount first and then log out:
sudo umount /mnt/iscsi
sudo iscsiadm -m node -T iqn.2026-09.com.example:storage.disk1 -p 10.0.0.10 --logout
Troubleshooting
iscsiadm: cannot make connection to 10.0.0.10: Connection refused or a timeout. The portal is not listening on that address, or the firewall blocks it. On the target, check sudo ss -tlnp | grep 3260 and sudo ufw status.
Login failed due to authorization failure. The initiator name is not in the ACL, or the CHAP credentials do not match. Compare /etc/iscsi/initiatorname.iscsi on the client with sudo targetcli ls /iscsi on the target, and restart iscsid after changing the name.
The target configuration is gone after a reboot. You forgot sudo targetcli saveconfig, or rtslib-fb-targetctl is not enabled. Check both, and look for errors with sudo journalctl -u rtslib-fb-targetctl.
The client hangs at boot waiting for the mount. The fstab entry is missing _netdev. Add it, together with nofail, and run sudo systemctl daemon-reload.
Initiator messages go to the journal: sudo journalctl -u iscsid -u open-iscsi and sudo dmesg | grep -i iscsi.
Conclusion
You exported a disk from an Ubuntu 24.04 server with the LIO iSCSI target, limited it to one initiator with an ACL and CHAP, and mounted it on a client that reconnects automatically after a reboot. As next steps, you can add a second portal on another network and configure multipath-tools on the initiator for path redundancy, back the LUN with a ZFS volume or LVM logical volume for snapshots, or add more LUNs to the same target for other clients, each with its own ACL.
