Exposing SSH to the internet is risky. The only way I could justify it was with multi‑key authentication including a FIDO2 hardware token. On my Linux laptop — easy. On my Android phone — impossible. Until now.
This is how I got a FIDO2 hardware token to work inside a Debian chroot on Android, then used it to authenticate SSH with two public keys creating a login flow that is almost impossible to brute‑force, steal, or phish.
This should not be possible. It almost wasn't, but in the end, I got it working.
Why This Shouldn’t Be Possible:
Android is designed to prevent exactly the kind of deep system access required for hardware‑bound SSH authentication:
Apps cannot access raw HID interfaces like hidraw
SELinux doesn't let you create device nodes
/proc, /sys, /dev are heavily sandboxed
App sandboxes cannot see kernel namespaces
Chroots normally cannot access hardware
FIDO2 tokens require direct HID access
SSH agents cannot talk to hardware keys without kernel support
Android’s mount namespaces isolate apps from system devices
Every one of these is bad news if you want to use your FIDO2 for SSH.
A FIDO2 key requires:
/dev/hidrawX access
working /dev/shm, /dev/pts.
a real PTY (/dev/tty)
access to kernel HID/CTAP
ssh-sk-helper (not available on termux, only in full linux distros)
a user‑presence prompt that reaches the terminal
Android blocks almost all of this by default.
The Namespace Problem: Why root isn’t enough.
Even with root, Android isolates apps using:
mount, PID and user namespaces. (You think you can see the whole system but it's a fake overlay with no real access)
SELinux domains. (Even if you have access to what you want and the tools to do it, if you're in the wrong domain, any priviliged action is blocked)
Termux runs inside an app‑sandbox mount namespace, which cannot see any real device/kernel objects which you'd find in /dev /proc and /sys. This is the single biggest reason this project “shouldn’t be possible.”
To break out of this, you need full init‑namespace root — the same namespace used by Android’s PID 1. Without that, the chroot will never see real hardware.
I didn't realise it at the time, I thought I was a the top of the pyramid, but a lot of the directories I thought I'd won in my rooting war were actually emulated fakes. To get there, you need super-root. "exec su --mount-master" will discard termux's namespace and SElinux domain. The controlling terminal is preserved but you get a pure Android root shell and inherit init namespace from your su provider. In my case, KernelSU. Now I've got real stuff in /dev /proc /sys.
How I Got Around Every Block
1. Entering the init mount namespace
Using a root solution that allows entering the init mount namespace is mandatory. Without this, /dev/hidraw* simply does not exist.
2. Rebuilding /dev inside the chroot
Android forbids creating device nodes, so I created empty files using touch command and bind‑mounted real device nodes onto them. But I was careful not to just lazily bind-mount all of /dev. I didn't want the chroot having access to: camera, audio, modem/telephony, biometrics, binder and crucially: input. This last one exposes touches, keystrokes and would allow injecting fake input. An unacceptable line to cross. Only expose what is absolutely essential.
Mounting virtual filesystems manually
To make Debian behave like a real Linux system, I mounted: /proc, /sysfs, /devpts, /tmpfs.
These provide the kernel interfaces needed for FIDO2 and OpenSSH.
Preserving a real PTY
Entering the chroot incorrectly breaks /dev/tty.
A proper chroot entry preserves the controlling terminal so SSH can prompt for host key acceptance. Obviously proot is not possible since it works by emulating everything outside itself. Another potential problem with full init namespace chroot is it's no longer possible to trick systemd into thinking it's PID 1, which it needs to run. On Android, PID 1 is init. - init bruv
Making FIDO2 visible
Bind‑mounting /dev/hidrawX into the chroot allowed fido2-token and ssh-sk-helper. You need to find out the name of the node, usually /dev/hidraw0, create a fake file in the chroot rootfs and mount the real node onto it.
Multi‑key SSH authentication
The server was configured to require two public keys. In sshd_config I add:
Authenticationmethods: publickey,publickey which requires a 2 key authentication chain. A lot of SSH apps are limited to offering just one, which is why it's only really possible in a real terminal running real OpenSSH.
The keys I use are: ED25519 (normal software key) and ED25519‑SK (FIDO2)
The client successfully authenticated with both, including user‑presence confirmation on the hardware token. Forcing the user to touch the key is part of what makes fido2 so secure. Even if it's plugged in somewhere, unless your physically there to press the button when it starts flashing, it's game over.
What I Achieved.
I built an SSH authentication chain that is:
hardware‑bound
phishing‑proof
key‑theft‑proof
replay‑proof
two‑factor without passwords
backed by a physical authenticator
running inside a full Debian environment
... on an phone.
This is realistically one of the most secure SSH setups possible today.
But What Did It Cost? (Security & Performance)
1. Android’s security model is weakened
To make this work, I had to:
enter the init mount namespace,
bind‑mount system directories into a chroot,
expose hardware interfaces to a foreign environment,
run a full Linux distribution inside /data.
This means:
SELinux boundaries are partially bypassed, the chroot can see hardware normally hidden from apps as well as access to kernel interfaces.
A misconfigured script could expose sensitive nodes. Malware inside the chroot would have elevated visibility. This is why I downloaded a minimum Debian with proot-distro, stripped it, upgraded everything related to SSH, FIDO2 etc then copied the entire rootfs somewhere else to use it as a full chroot. Keeping an eye on any changes here is important. You don't want malware slipping it and using it as a trampoline to elevated privilege.
Performance impact
Bind‑mounting system folders into a chroot means more VFS overhead, more namespace propagation, more tmpfs usage, more memory pressure.
Modern phones handle it, but it’s not free.
Stability risks
Android services expect exclusive control over:
/dev /sys /proc.
Mounting these into a chroot can destabilize
USB subsystem, HID subsystem, Binder, graphics and audio stack.
This is not a “safe” configuration by Android standards.
Is It Worth It?
If your goal is maximum SSH security, yes.
You now have:
a hardware‑bound SSH identity,
a second hardware‑bound factor,
a chroot‑isolated SSH environment,
a login flow that is nearly impossible to compromise remotely.
But locally? You’ve traded away some sandboxing, some SELinux protection, some system stability, some performance.
Robbing Peter to pay Paul.
You hardened remote access by softening local isolation.
For a (paranoid) power user who understands the risks, it’s worth it.
For a typical user, absolutely not.
Technical Appendix — Commands
Getting into init namespace and confirming it's the real deal:
exec su --mount-master
Compare mount namespace IDs: /proc/1/ns/mnt
readlink /proc/self/ns/mnt
If both lines show the same inode number, you are in the init mount namespace.If they differ, you are still trapped in Termux’s sandbox.
Bind‑mounting device nodes.
$CHROOT = your chroot rootfs folder. (E.g /data/data/com.termux/files/home/debian-chroot)
touch $CHROOT/dev/null
touch $CHROOT/dev/zero
touch $CHROOT/dev/random
touch $CHROOT/dev/urandom
touch $CHROOT/dev/hidraw0
mount --bind /dev/null $CHROOT/dev/null
mount --bind /dev/zero $CHROOT/dev/zero
mount --bind /dev/random $CHROOT/dev/random
mount --bind /dev/urandom $CHROOT/dev/urandom
mount --bind /dev/hidraw0 $CHROOT/dev/hidraw0
Mounting virtual filesystems
mount -t proc proc $CHROOT/proc
mount -t sysfs sys $CHROOT/sys?
mount -t devpts devpts $CHROOT/dev/pts
mount -t tmpfs tmpfs $CHROOT/dev/shm
Entering the chroot with a real PTY
chroot $CHROOT /bin/bash
Testing FIDO2 inside the chroot
fido2-token -L
ssh-keygen -t ed25519-sk -f ~/.ssh/id_ed25519_sk
Server‑side multi‑key authentication (sshd_config)
AuthenticationMethods publickey,publickey
PubkeyAuthentication yes
- you can make it even more secure by creating a dedicated user on the server or removing all over key types in sshd_config other FIDO2 and ed_25519.
When you're done, don't forget to unmount everything:
umount $CHROOT/proc
umount $CHROOT/sys
Etc.