| |

LFCE 113 🐧 iSCSI Target Setup

ā±ļø Estimated read: 14 min | |

The iSCSI target is the server side of the block storage relationship. It exports LUNs—logical unit numbers backed by block devices, files, or RAM disks—and presents them to initiators over TCP port 3260. On modern Linux, this role is filled by LIO (LinuxIO), the in-kernel target subsystem, configured through the targetcli shell . LIO has been part of the kernel since the 2.6.38 era, and the userspace tooling has stabilized around the free-branch packages: targetcli-fb, python-rtslib-fb, and python-configshell-fb .

This chapter covers the complete target setup workflow: installing the tooling, creating backstores, defining a target IQN, attaching LUNs, configuring portals, restricting access with ACLs, enabling CHAP authentication, and persisting the configuration across reboots. By the end, you will have a target that an initiator can discover, log in to, and use as a block device—with authentication enabled and the configuration surviving a restart.

Key point: targetcli writes directly to ConfigFS in the kernel. Changes take effect immediately, but they are lost on reboot unless saved with saveconfig and restored by the target service at boot. The save-and-enable step is not optional.


Why target setup has this shape

ConfigFS is the real configuration store. targetcli is a shell over ConfigFS, a kernel filesystem mounted at /sys/kernel/config. Every create command you issue creates a directory in ConfigFS, and the kernel reacts to that directory existing . This is why changes are immediate: there is no separate daemon to reload. It is also why saveconfig exists: ConfigFS is in-memory, and the saved JSON file at /etc/target/saveconfig.json is what the target service reads at boot to recreate the tree .

The backstore is separate from the target. A backstore is a storage object: a block device, a file, a RAM disk, or a passthrough SCSI device. It exists under /backstores/ and has no relationship to any target until you explicitly attach it as a LUN under a target’s TPG . This separation lets you create the storage once and map it to multiple targets or ACLs if needed.

The TPG is the configuration unit. When you create a target, targetcli automatically creates one Target Portal Group (TPG) named tpg1 . Almost all setups need only one. The TPG holds the LUNs, the portals (IP:port listeners), and the ACLs (initiator permissions). CHAP settings can live at the TPG level or per-ACL, and the generate_node_acls attribute decides which takes precedence .

ACLs are the authorization boundary. An ACL is created for a specific initiator IQN. Without an ACL, an initiator that authenticates may still see no LUNs, because the LUN mapping must exist under the ACL (or generate_node_acls must be enabled to fall back to TPG-wide mappings) . The safe default is generate_node_acls=0, which means only explicitly listed initiators get access .


a. Installing targetcli and starting the service

The package name varies by distribution, but the free-branch version is the maintained one. On RHEL-family systems it is targetcli; on Debian and Arch it is targetcli-fb .

# RHEL / CentOS / Oracle Linux
sudo dnf install targetcli

# Debian / Ubuntu
sudo apt install targetcli-fb

# Arch
sudo pacman -S targetcli-fb

After installation, the target service must be enabled. This service loads the necessary kernel modules (target_core_mod, iscsi_target_mod), mounts ConfigFS, and restores the saved configuration .

sudo systemctl enable --now target

On Debian, the unit may be named rtslib-fb-targetctl.service depending on the version . Check with systemctl list-unit-files | grep target if the standard name is not found.

b. Creating a backstore

A backstore is the actual storage. The two common types are block for block devices and fileio for file-backed LUNs. Block devices include physical disks, partitions, LVM volumes, and RAID arrays .

# Enter the targetcli shell
sudo targetcli

# Create a block backstore from a partition
/> cd /backstores/block
/backstores/block> create name=disk01 dev=/dev/sdb1

# Or create a file-backed backstore (200 MB image)
/> cd /backstores/fileio
/backstores/fileio> create name=file01 file_or_dev=/var/lib/iscsi/disk01.img size=200M

For file-backed storage, Red Hat recommends write_back=false to avoid data loss from the local filesystem cache . The block backstore type avoids that layer entirely by exporting the block device directly.

c. Creating the target and attaching LUNs

Creating a target generates an IQN automatically, or you can supply your own. The IQN follows the format iqn.YYYY-MM.reverse-domain:optional-suffix .

# Create a target with a generated IQN
/> cd /iscsi
/iscsi> create
Created target iqn.2024-01.com.example:storage.disk01
Created TPG 1.

# Or specify the IQN explicitly
/iscsi> create iqn.2024-01.com.example:storage.disk01

Attaching the backstore as a LUN creates the link between the storage object and the target. The LUN number is assigned automatically if you omit it .

# Navigate to the TPG's luns directory
/iscsi> cd iqn.2024-01.com.example:storage.disk01/tpg1/luns
/iscsi/.../tpg1/luns> create /backstores/block/disk01
Created LUN 0.

The target now has a LUN, but no portal and no ACL. An initiator cannot reach it yet.

d. Configuring portals and ACLs

A portal is an IP address and port the target listens on. When a target is created, targetcli automatically creates a default portal listening on all IPv4 and IPv6 addresses (::0) on port 3260 . In production, you typically replace this with a specific IP so the target does not listen on management or public interfaces.

# Navigate to portals
/iscsi/.../tpg1> cd portals

# Delete the default wildcard portal
/iscsi/.../tpg1/portals> delete 0.0.0.0 3260

# Create a portal on a specific storage-network IP
/iscsi/.../tpg1/portals> create 192.168.10.20 3260

The ACL lists the initiator IQNs allowed to connect. Without an ACL and with generate_node_acls=0, no initiator can log in, regardless of CHAP .

# Navigate to acls
/iscsi/.../tpg1> cd acls

# Create an ACL for the initiator's IQN
/iscsi/.../tpg1/acls> create iqn.2024-01.com.example:client01

Creating the ACL automatically maps the TPG’s LUNs to it. The initiator now sees LUN 0 when it logs in.

e. Enabling CHAP authentication

CHAP is enabled per-ACL or at the TPG level. The most common production configuration is one-way CHAP (target authenticates initiator) with generate_node_acls=0, so each ACL carries its own credentials.

# Navigate to the ACL
/iscsi/.../tpg1/acls> cd iqn.2024-01.com.example:client01

# Set the CHAP credentials the initiator must use
/iscsi/.../acls/iqn...client01> set auth userid=client01
/iscsi/.../acls/iqn...client01> set auth password=SecurePass123

For mutual CHAP, add the reverse credentials that the target will present to the initiator :

/iscsi/.../acls/iqn...client01> set auth mutual_userid=storage01
/iscsi/.../acls/iqn...client01> set auth mutual_password=TargetPass456

Ensure authentication is enabled on the TPG and ACL enforcement is active :

/iscsi/.../tpg1> set attribute authentication=1
/iscsi/.../tpg1> set attribute generate_node_acls=0

f. Saving and persisting the configuration

targetcli exits with an automatic save by default (auto_save_on_exit is true), but running saveconfig explicitly is harmless and removes ambiguity . The saved file is /etc/target/saveconfig.json .

# From the root of the targetcli tree
/> saveconfig
/> exit

The target service, which you enabled earlier, reads this file at boot. Verify it is enabled:

sudo systemctl is-enabled target
# Output: enabled

Because the saved file contains CHAP secrets in a recoverable form, restrict its permissions :

sudo chmod 600 /etc/target/saveconfig.json

Complete Example Session

# ============================================
# PART 1: INSTALL AND ENABLE THE SERVICE
# ============================================
sudo dnf install -y targetcli
sudo systemctl enable --now target

# Verify modules loaded and ConfigFS mounted
targetcli status
# ============================================
# PART 2: CREATE A BLOCK BACKSTORE
# ============================================
sudo targetcli

/> cd /backstores/block
/backstores/block> create name=disk01 dev=/dev/sdb1
# Output: Created block storage object disk01 using /dev/sdb1.
# ============================================
# PART 3: CREATE THE TARGET
# ============================================
/> cd /iscsi
/iscsi> create iqn.2024-01.com.example:storage.disk01
# Output: Created target iqn.2024-01.com.example:storage.disk01.
#         Created TPG 1.
# ============================================
# PART 4: ATTACH THE LUN
# ============================================
/iscsi> cd iqn.2024-01.com.example:storage.disk01/tpg1/luns
/iscsi/.../tpg1/luns> create /backstores/block/disk01
# Output: Created LUN 0.
# ============================================
# PART 5: CONFIGURE A SPECIFIC PORTAL
# ============================================
/iscsi/.../tpg1/luns> cd ../portals
/iscsi/.../tpg1/portals> delete 0.0.0.0 3260
/iscsi/.../tpg1/portals> delete ::0 3260
/iscsi/.../tpg1/portals> create 192.168.10.20 3260
# Output: Created network portal 192.168.10.20:3260.
# ============================================
# PART 6: CREATE AN ACL FOR THE INITIATOR
# ============================================
/iscsi/.../tpg1/portals> cd ../acls
/iscsi/.../tpg1/acls> create iqn.2024-01.com.example:client01
# Output: Created Node ACL for iqn.2024-01.com.example:client01
#         Created mapped LUN 0.
# ============================================
# PART 7: ENABLE ONE-WAY CHAP
# ============================================
/iscsi/.../tpg1/acls> cd iqn.2024-01.com.example:client01
/iscsi/.../acls/iqn...client01> set auth userid=client01
/iscsi/.../acls/iqn...client01> set auth password=SecurePass123
# ============================================
# PART 8: ENFORCE ACL-BASED AUTHENTICATION
# ============================================
/iscsi/.../acls/iqn...client01> cd ../..
/iscsi/.../tpg1> set attribute authentication=1
/iscsi/.../tpg1> set attribute generate_node_acls=0
# generate_node_acls=0 means ACLs are enforced;
# initiators without an ACL cannot log in.
# ============================================
# PART 9: SAVE AND EXIT
# ============================================
/iscsi/.../tpg1> cd /
/> saveconfig
# Output: Current configuration saved to /etc/target/saveconfig.json
/> exit
# ============================================
# PART 10: VERIFY PERSISTENCE AND PERMISSIONS
# ============================================
sudo systemctl is-enabled target
# Output: enabled

sudo chmod 600 /etc/target/saveconfig.json
sudo ls -l /etc/target/saveconfig.json
# Output: -rw-------. 1 root root ... saveconfig.json

# Optional: restart the service and confirm the target returns
sudo systemctl restart target
targetcli ls /iscsi

The ten parts covered installation and service enablement, backstore creation, target definition, LUN attachment, portal restriction, ACL creation, one-way CHAP, ACL enforcement, configuration persistence, and permission hardening.


Quick Reference

The targetcli object tree

PathContents
/backstores/blockBlock device backstores
/backstores/fileioFile-backed backstores
/iscsi/<iqn>/tpg1/lunsLUN mappings for the TPG
/iscsi/<iqn>/tpg1/portalsIP:port listeners
/iscsi/<iqn>/tpg1/aclsInitiator ACLs

The setup commands

CommandEffect
create name=X dev=/dev/sdX (in backstores)Create block backstore
create iqn... (in /iscsi)Create target and TPG
create /backstores/block/X (in luns)Attach backstore as LUN
create <IP> 3260 (in portals)Listen on specific IP
create iqn... (in acls)Authorize an initiator
set auth userid=... password=... (in ACL)Set CHAP credentials
set attribute authentication=1 (in TPG)Enable CHAP enforcement
saveconfig (at root)Persist to /etc/target/saveconfig.json

Authentication attributes

AttributeLocationEffect
authentication=1TPGRequire CHAP on normal sessions
generate_node_acls=0TPGEnforce per-ACL mappings and CHAP
generate_node_acls=1TPGIgnore ACLs, use TPG-wide settings
discovery_auth/iscsiRequire CHAP for SendTargets

Persistence files

FilePurpose
/etc/target/saveconfig.jsonSaved configuration (contains secrets)
target.serviceRestores configuration at boot
/sys/kernel/config/targetLive ConfigFS tree (in-memory)

Best Practices

āœ… Do This:

# Set generate_node_acls=0 to enforce ACL mappings
set attribute generate_node_acls=0                         # āœ…
# Restrict saveconfig.json permissions (contains CHAP secrets)
chmod 600 /etc/target/saveconfig.json                      # āœ…
# Replace the default wildcard portal with a specific storage-network IP
delete 0.0.0.0 3260 && create 192.168.10.20 3260           # āœ…
# Use a CHAP secret of at least 12 bytes
set auth password=SecurePass123                            # āœ…
# Explicitly run saveconfig before exit
saveconfig                                                 # āœ…
# Verify the target service is enabled
systemctl is-enabled target                                # āœ…

āŒ Don’t Do This:

# Leave generate_node_acls=1 in production
set attribute generate_node_acls=1                         # āŒ (ignores ACLs)
# Leave saveconfig.json world-readable
chmod 644 /etc/target/saveconfig.json                      # āŒ
# Use a short or trivial CHAP secret
set auth password=12345                                    # āŒ
# Assume ConfigFS changes survive reboot without saveconfig
# They do not; the kernel tree is in-memory
# exit without saveconfig                                  # āŒ
# Listen on 0.0.0.0 on a host with public interfaces
create 0.0.0.0 3260                                        # āŒ (default portal)
# Skip ACL creation and rely on network isolation alone
# Any initiator that can reach port 3260 may attempt login
# (no ACLs)                                                # āŒ

Common Pitfalls

PitfallWhy It HappensFix
Configuration lost after rebootsaveconfig not run or target service not enabledRun saveconfig and systemctl enable target
Initiator logs in but sees no LUNsLUN mapped under TPG but not under ACL, with generate_node_acls=0Create the ACL; LUN mapping is automatic, or map explicitly
Initiator cannot log in at allNo ACL for its IQNCreate ACL with the initiator’s IQN
CHAP fails despite correct passwordauthentication=0 on TPG, or wrong CHAP directionSet authentication=1; verify one-way vs mutual
Target listens on unwanted interfacesDefault wildcard portal (::0) left in placeDelete it and create a specific IP portal
targetcli changes vanish on exitauto_save_on_exit disabled or not savedRun saveconfig explicitly before exit

Real-World Examples

1. Create a block backstore

/backstores/block> create name=disk01 dev=/dev/sdb1

2. Create a fileio backstore with write-through

/backstores/fileio> create name=file01 file_or_dev=/var/lib/iscsi/disk.img size=1G write_back=false

3. Create a target with an explicit IQN

/iscsi> create iqn.2024-01.com.example:storage.disk01

4. Attach a LUN

/iscsi/.../tpg1/luns> create /backstores/block/disk01

5. Replace the default portal

/iscsi/.../tpg1/portals> delete 0.0.0.0 3260
/iscsi/.../tpg1/portals> create 192.168.10.20 3260

6. Create an initiator ACL

/iscsi/.../tpg1/acls> create iqn.2024-01.com.example:client01

7. Set one-way CHAP

/iscsi/.../acls/iqn...client01> set auth userid=client01
/iscsi/.../acls/iqn...client01> set auth password=SecurePass123

8. Enforce ACLs and authentication

/iscsi/.../tpg1> set attribute authentication=1
/iscsi/.../tpg1> set attribute generate_node_acls=0

9. Save configuration

/> saveconfig

10. Verify persistence

systemctl is-enabled target && ls -l /etc/target/saveconfig.json

Visual

The targetcli object tree

ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│  TARGETCLI CONFIGURATION TREE                                │
│                                                              │
│  /                                                           │
│  ā”œā”€ā”€ backstores/                                             │
│  │   ā”œā”€ā”€ block/                                              │
│  │   │   └── disk01  ← /dev/sdb1                             │
│  │   └── fileio/                                             │
│  │       └── file01  ← /var/lib/iscsi/disk.img               │
│  │                                                           │
│  └── iscsi/                                                  │
│      └── iqn.2024-01.com.example:storage.disk01              │
│          └── tpg1/                                           │
│              ā”œā”€ā”€ luns/                                       │
│              │   └── lun0  → /backstores/block/disk01        │
│              ā”œā”€ā”€ portals/                                    │
│              │   └── 192.168.10.20:3260                      │
│              └── acls/                                       │
│                  └── iqn.2024-01.com.example:client01        │
│                      └── CHAP credentials                    │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜

The access control layers

ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│  ACCESS CONTROL LAYERS                                       │
│                                                              │
│  Layer 1: Network reachability                               │
│    Can the initiator reach 192.168.10.20:3260?               │
│                                                              │
│  Layer 2: ACL existence                                      │
│    Is the initiator's IQN in the ACL list?                   │
│    (enforced when generate_node_acls=0)                      │
│                                                              │
│  Layer 3: CHAP authentication                                │
│    Does the initiator know the correct password?             │
│    (enforced when authentication=1)                          │
│                                                              │
│  Layer 4: LUN mapping                                        │
│    Which LUNs are mapped to this ACL?                        │
│                                                              │
│  All four must pass for the initiator to see storage.        │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜

Persistence flow

ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│  CONFIGFS TO PERSISTENCE                                     │
│                                                              │
│  targetcli create ──► ConfigFS (/sys/kernel/config/target)  │
│       │                    │                                 │
│       │                    ā–¼                                 │
│       │              Kernel applies immediately              │
│       │                                                      │
│       ā–¼                                                      │
│  saveconfig ──► /etc/target/saveconfig.json                  │
│                      │                                       │
│                      ā–¼                                       │
│              target.service at boot                          │
│                      │                                       │
│                      ā–¼                                       │
│              restoreconfig ──► ConfigFS rebuilt              │
│                                                              │
│  Without saveconfig + enabled service, the target            │
│  disappears on reboot.                                       │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜

One-way vs mutual CHAP

ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│  CHAP DIRECTIONS                                             │
│                                                              │
│  One-way (most common):                                      │
│    Target ──challenge──► Initiator                           │
│    Target ◄──response─── Initiator                           │
│    Target verifies initiator's password.                     │
│    Initiator does not verify target.                         │
│                                                              │
│  Mutual:                                                     │
│    Target ──challenge──► Initiator                           │
│    Target ◄──response─── Initiator                           │
│    Initiator ──challenge──► Target                           │
│    Initiator ◄──response─── Target                           │
│    Both sides verify each other.                             │
│                                                              │
│  Mutual requires mutual_userid and mutual_password           │
│  in addition to userid and password.                         │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜

Summary

ItemValue
Target subsystemLIO (LinuxIO), in-kernel since 2.6.38
Configuration tooltargetcli (free-branch: targetcli-fb)
Backstore typesblock (device), fileio (file), ramdisk, pscsi
Default iSCSI portTCP 3260
Target identityIQN, created with create in /iscsi
TPGCreated automatically as tpg1 with each target
ACLPer-initiator authorization, created by IQN
CHAPset auth userid=... password=... in ACL
Enforcementauthentication=1, generate_node_acls=0
Persistence file/etc/target/saveconfig.json
Boot servicetarget.service (or rtslib-fb-targetctl.service)

Key takeaways:

  • ConfigFS is live, but not persistent. Every targetcli change applies immediately to the kernel. saveconfig and an enabled target service are what make it survive a reboot.
  • Backstores and targets are separate objects. Create the storage first, then attach it as a LUN under a specific TPG.
  • generate_node_acls=0 is the production setting. It forces per-ACL LUN mapping and CHAP, so an initiator without an explicit ACL cannot log in.
  • The default portal listens on everything. Replace ::0 with a specific storage-network IP to avoid exposing iSCSI on management or public interfaces.
  • CHAP credentials live in the ACL or the TPG. With generate_node_acls=0, set them per-ACL. With =1, TPG-wide settings are used instead, which is acceptable only in test environments.
  • The saved configuration contains secrets. Restrict /etc/target/saveconfig.json to mode 600.

Remember: iSCSI target setup is a sequence of four layers: storage (backstore), exposure (LUN under TPG), reachability (portal), and authorization (ACL plus CHAP). Each layer is independent in targetcli, and skipping one produces a target that exists but is unusable. The most common failure is an initiator that can reach the portal but sees no LUNs, because the LUN was attached to the TPG but no ACL was created for that initiator. Create the ACL, verify generate_node_acls=0, save the configuration, and enable the target service. That combination produces a target that works now and still works after the next reboot.

Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

šŸ”Ž Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
āœ… Finance (Mortgage, Interest, Inflation)
āœ… Tech (Base64, JSON, Dev Suite, IP)
āœ… Health (BMI, BMR, TDEE)
āœ… Productivity (Timer, Workspace, QR)

āš”ļø Fast & Private
šŸ”’ No data leaves your device
šŸ’Ž 100% Free

šŸ”— Use it now: https://tools.kandz.me
šŸ”– Bookmark it—you’ll need it later!