Turning old computers into Chrome web kiosks with LTSP


The school districts I support have tens of thousands of dollars invested in student workstations, many of which are well beyond their refresh cycle. Funding trickles in, but well behind the 1:1 student computing initiatives that administrators push for.

The aging machines still physically work, but most of them:

  • Run end-of-life Windows XP, and are not ready for Windows 8.1;
  • Cost more to maintain than the cost of replacement;
  • Don’t need to run local software the way they did in mid-2000s.

Students these days, more often than not, just need a way to get online. Most curriculum and learning apps are web-based, and with Google Apps or Office 365, even productivity comes through a browser. The era of myriad local client software has ended, and those use cases which require specialized software have become the exception instead of the rule. This brings a lot of attractiveness to a simple way to turn these older computers into lean web kiosks. Enter LTSP.

Skip straight to the fun stuff

About LTSP

LTSP is a suite of free software, running on top of a Linux server, which can turn old computers into thin clients. Depending on what you want to do, the thin client hardware doesn’t need a lot of horsepower, especially by modern standards. By default, LTSP provides a full remote desktop or terminal services solution, with thin clients logging into a full desktop session with all the programs they might expect on a local machine, except everything really runs server-side.

This configuration is pretty complex and puts a lot of burden on the server, since all clients’ processes are run centrally. Of course, this does give an administrator full control over every users’ session, and offers much of the functionality of a modern VDI setup. However, it’s far from ideal for my use case. I don’t need a full desktop suite—just full-screen Chrome.

After a bit of elbow-grease, I ended up with a way to turn nearly any workstation into a network-booting thin client that runs only a full-screen, full-featured, fully customized Chrome browser, directly in RAM.


  1. Install LTSP packages on a server running your Linux distribution of choice
  2. Install and customize a client chroot  on the server, which will become the environment clients PXE boot into
  3. Either set up DHCP on the LTSP server, or set options 66 and 67 on your existing DHCP server to point to the LTSP server
  4. Set thin clients to PXE boot by default


  • All client software is installed configured centrally
  • Everything runs fast, directly in RAM
  • Updates are universal and require merely a client reboot to apply
  • Client machines only need to be touched once—to change them to PXE boot by default
  • Chrome comes with many policy options to tailor it to specific use cases
  • Nothing to install on client machines (you don’t even need local storage of any kind)
  • Easy to replace failed hardware
  • Almost any workstation made in the last 10 years is powerful enough
  • No irreversible change is made to clients—if something fails, just change the boot order back
  • $0 licensing cost for both clients and server
  • Scales extremely well—a basic setup can handle hundreds of clients


  • Requires configuring and running a dedicated Linux server
  • Requires a fairly robust wired infrastructure: 1Gbps at the server and 100Mbps to the client
  • Only works with wired clients that support PXE booting (wireless clients need not apply)
  • Printing, USB drive and network share access are not available in this configuration (although it wouldn’t take much tinkering to get them working, either)

System Requirements


The server boils down to little more than a file server. Booting clients will tax disk and network speed briefly, and compressing chroot images will take some CPU cycles, but overall the hardware requirements are minimal. I ran a successful beta test for months on a reclaimed Dell workstation.

  • 1Gbps NIC
  • ~30GB disk space, depending on how many chroots you want
  • 2GB RAM
  • Fairly modern CPU
  • DHCP server (can run on the LTSP server, but doesn’t have to)


Almost any machine made in the last 10 years has what it takes. Thanks to the versatility of the Linux kernel, almost all hardware I’ve run it on has worked flawlessly out of the box, including video, audio and input devices.

  • 100Mbps NIC capable of PXE booting, or something like iPXE installed
  • 512MB RAM, although 2GB or more is ideal
  • Core2 CPU or better for smooth full-screen HD video playback; anything else can run on a Pentium 4-era CPU easily
  • No local storage is necessary

Technical Details

  • After PXE booting, clients receive DHCP options 66 and 67, which they use to bootstrap into a minimal Linux filesystem image mounted via NBD on the server
  • After bootstrapping some config files, the LTSP system starts a kiosk screen script, which causes the clients to pull the Chrome browser into RAM and execute locally (technically making this not a true “thin client,” but more of a network-booting, minimal fat client)
  • The browser and its cache run directly in RAM, hence why 2GB or more is ideal, although I have seen good results on as little as 512MB
  • The root filesystem remains persistently mounted via NBD, loading stuff into RAM as needed
  • These persistent NBD connections are trivial in terms of network and I/O impact; one server can handle hundreds of clients
  • Client web traffic is routed normally as any other network node
  • A separate VLAN for thin clients is not required
  • During boot, clients tax the servers’ disk and network briefly. Booting more than 20 clients simultaneously may noticeably delay boot times, but not by much. Staggering bootup or teaming multiple server NICs is always an option if this kind of parallel boot is required

Getting Started

I recommend getting your feet wet in a virtual environment. For those who already know about LTSP and just want client configuration specifics, skip ahead.

In my case, I used VirtualBox right on my laptop, but this would work with VMware Workstation, qemu or probably any other hypervisor.

Choose your Distro

Ubuntu is a popular choice, which is what I started with. Debian is another good choice. Unless you know what you’re doing, I suggest starting with either of these. I also recommend using the amd64 (x86_64 or 64-bit) version, since you can still create i386/32-bit client chroots.

 (Optional) Build a Virtual Testbed

  1. Create the virtual network
    1. Open VirtualBox Preferences
    2. Click Networks
    3. Click Host-only networks
    4. Click the plus icon to add a host-only network
    5. Click the wrench icon to edit it
    6. In the IP address field, give it a network that doesn’t exist on the network you’re really on. I chose Leave netmask at
    7. Click DHCP Server, and uncheck Enable Server
  2. Create the server VM
    1. Download the ISO for your distribution of choice
    2. Create a new server virtual machine
      1. Give it a name
      2. Give it some RAM—512MB or 1GB is fine
      3. Give it a hard drive—at least 30GB, preferably more, depending on how many chroots you want to have at any given time
    3. Load the ISO into its virtual CD drive
      1. Select VM
      2. Settings
      3. Storage
      4. Click the CD icon which says Empty
      5. Under Attributes, next to CD/DVD Drive, click the CD icon
      6. Choose a virtual CD/DVD disk file…
      7. Find your ISO
    4. Set up the virtual NICs
      1. Still under settings, click Network
      2. Under Adapter 1, you can either leave it on NAT or switch to Bridged Adapter, which will bridge it to your real network
      3. Click Enable Network Adapter
      4. Select Attached to: Host-only adapter
      5. Select name: vboxnet0 (or whatever the name is from setting up the virtual network)
  3. Create a thin client VM
    1. Create a new virtual machine
      1. Give it a name
      2. Give it some RAM—512MB is plenty
      3. Don’t give it a hard drive
    2. Set up the thin client’s network properties
      1. In Settings, click System
      2. In Boot Order, uncheck everything except Network
      3. Click Network
      4. In Attached to, select Host-only adapter
      5. In Name, make sure it picks the host-only network you just created, e.g. vboxnet0

 Install and Prepare the Server OS

  • Boot the server VM
  • Install Ubuntu server with all defaults, unless you know for sure you want or need something different
  • When prompted, choose SSH server, which will make it much easier to administrate the system
  • Set appropriate hostname, network and IP settings for your environment. If you are building the virtual testbed, leave everything as-is and see below for network config.
  • Ensure SSH is running
    sudo service ssh status
  • If appropriate, add static DNS A records for the server, e.g.: ltsp.example.org
  • Update system
    sudo apt-get update && sudo apt-get upgrade

Install and Configure LTSP

  • Install LTSP packages
    sudo apt-get install ltsp-server-standalone
  • Build the client chroot. This may take an hour or more.
    sudo ltsp-build-client --arch i386 --fat-client --kiosk --skipimage --late-packages devilspie,metacity

Let’s break this down.

  • The ltsp-build-client command does exactly as it implies: builds a new LTSP client chroot. There are a lot of options I haven’t looked at. Check the manpage for more details.
  • --arch i386 builds a 32-bit chroot. If you are 100% sure all thin clients have the x86_64 CPU instruction set, you can go with --arch amd64.
  • According to the manpage, --fat-client sets the chroot to run all processes within the local client, as opposed to running them from the server. I’m not sure this switch is necessary, but it doesn’t hurt.
  • The --kiosk switch ostensibly makes adjustments appropriate for web kiosks, as opposed to the more traditional full-featured desktop experience.
  • --skipimage will skip the final step of building the squashfs image of the resulting system. We want to do this so we can make some changes below, before building it manually.
  • --late-packages devilspie,metacity installs the devilspie and metacity packages after pulling all the other chroot packages. These are necessary for running Chrome full-screen.

Install and Customize Chrome

  • Drop into the chroot
    sudo ltsp-chroot -ma i386
  • Install vim (or your editor of choice)
    apt-get install vim
  • Download and install Chrome
    cd /tmp
    wget https://dl.google.com/linux/direct/google-chrome-stable_current_i386.deb
    dpkg -i google-chrome-stable_current_i386.deb (this will fail, this next command fixes it)
    apt-get -f install
  • Patch the kiosk script so it will run Chrome fullscreen
    vim /usr/share/ltsp/kioskSession
    Update lines 13 and 14 like this
    case $KIOSK_EXE in
    Update line 19 like this, changing “fullscreen” to “maximize”
    echo "(if (matches (window_name) \"$(basename ${KIOSK_EXE})\") (undecorate) (maximize))" > ${HOME}/.devilspie/default.ds
  • (Optional) Apply Chrome managed or recommended policies
    mkdir -p /etc/opt/chrome/policies/managed
    vim /etc/opt/chrome/policies/managed/00-default.json

You can get the exhaustive policy reference file from Google, or check out mine for reference.

 "AllowCrossOriginAuthPrompt": false,
 "AllowFileSelectionDialogs": false,
 "AllowOutdatedPlugins": true,
 "AlwaysAuthorizePlugins": true,
 "AudioCaptureAllowed": true,
 "AutoFillEnabled": false,
 "BookmarkBarEnabled": true,
 "CloudPrintSubmitEnabled": true,
 "DefaultBrowserSettingEnabled": false,
 "DeveloperToolsDisabled": true,
 "DiskCacheDir": "/tmp/Chrome_cache",
 "DiskCacheSize": 104857600,
 "ForceSafeSearch": true,
 "FullscreenAllowed": true,
 "HideWebStoreIcon": true,
 "HomepageLocation": "chrome://chrome-signin",
 "ImportAutofillFormData": false,
 "ImportBookmarks": false,
 "ImportHistory": false,
 "ImportHomepage": false,
 "ImportSavedPasswords": false,
 "ImportSearchEngine": false,
 "IncognitoModeAvailability": 0,
 "ManagedBookmarks": [
 {"name": "Sign In to Chrome", "url": "chrome://chrome-signin"},
 {"children": [
 {"name": "Gmail", "url": "https://gmail.com/"},
 {"name": "Google Drive", "url": "https://drive.google.com/"},
 {"name": "Google Classroom", "url": "https://classroom.google.com/"},
 {"name": "YouTube for EDU", "url": "https://www.youtube.com/education"}
 ], "name": "Google Apps"}
 "MetricsReportingEnabled": false,
 "PasswordManagerEnabled": false,
 "PrintingEnabled": true,
 "RestoreOnStartup": 4,
 "RestoreOnStartupURLs": ["chrome://chrome-signin"],
 //"RestrictSigninToPattern": "*@example.org",
 "SafeBrowsingEnabled": true,
 "TranslateEnabled": false

These are all semi-sane defaults for my use case, but be sure to make any relevant changes to your environment. Managed bookmarks are especially helpful in my case.

(Optional) Apply different Chrome settings profiles to subnets

This project is in production at a few school districts, and the request to give each school site different defaults has come up a few times. For example, the high school would would like a different home page and set of managed bookmarks than the elementary school. As far as I know, there is no native way to do this with LTSP or Chrome.

In a bit of spare time I came up with a basic solution. It assigns a site-specific Chrome policy if the thin client’s IP address is within a CIDR netblock assigned to that site. If the client’s IP isn’t in a known netblock, the default policy remains unaffected. This could be extended to customize settings based on any number of variables, not just the client’s IP address.

Assuming you’re already logged in to the chroot:

  • Install grepcidr, a handy tool which tells us if a given IP address falls within a list of subnets
    apt-get install grepcidr
  • Create some folders
    mkdir -p /usr/share/ltsp/profile-detect/sites
    mkdir /usr/share/ltsp/profile-detect/profiles
  • Add the profile detection script
    vim /usr/share/ltsp/profile-detect/profile-detect

    # LTSP profile detection
    # Read client's IP address
    # We need to save it to a file because of the way grepcidr behaves
    # when matching netblocks from files
    hostname -I | cut -d' ' -f1 > my-ip
    # Walk through each site's subnets
    for SITE in ${SITES_DIR}/*.site; do
    # For each site's subnet, see if my IP is in it
    grepcidr -f $SITE my-ip >/dev/null
    # If grepcidr returns a result, we've found the client's site
    if [ $? -eq 0 ]; then
    # Copy the site's profile config to the Chrome managed policy dir
    # Profile config name must match the site name and end in .json
    # E.g., example.site must have matching example.json
    # We prefix the profile config with "99-" so it takes precedence
    # over the default config
    MY_PROFILE=$(basename $SITE .site).json
    # Clean up
    rm my-ip
  • For each site you want to localize a profile for, create a file and add all of that site’s subnets in CIDR notation, one on each line. There is no limit to how many sites you can have, or subnets within those sites, although if you add the same subnet to multiple sites it obviously won’t work.
    echo "" >> /usr/share/ltsp/profile-detect/sites/example-1.site
    echo "" >> /usr/share/ltsp/profile-detect/sites/example-1.site
    echo "" >> /usr/share/ltsp/profile-detect/sites/example-2.site
    echo "" >> /usr/share/ltsp/profile-detect/sites/example-2.site
  • For each site you created subnets for, create a Chrome profile with matching site name. Chrome will apply the last policy it reads for a given setting. This means you only have to localize settings you want to override from the default policy. For example, if you wanted example-1.site to get a different homepage:
    vim /usr/share/ltsp/profile-detect/profiles/example-1.json

        "RestoreOnStartupURLs": [
  • Symlink the script to init.d
    ln -s /usr/share/ltsp/profile-detect/profile-detect /etc/init.d/profile-detect
  • Call the script in lts.conf (see next step)

 Finish the chroot

  • (Optional) Enable automatic selection of USB audio devices when plugged in. Since we use a lot of USB headsets, this is mandatory for us
    echo "load-module module-switch-on-connect" >> /etc/pulse/default.pa
  • Log out of the chroot
  • Update SSH keys and build the squashfs image
    sudo ltsp-update-sshkeys
    sudo ltsp-update-image i386
  • Create lts.conf
    sudo vim /var/lib/tftpboot/ltsp/i386/lts.conf

This file is the main LTSP config file. With it, you can control a lot from this one file, but in my case it’s pretty simple. Here is a minimal config that should get things going. Check man lts.conf for more options, or read the LTSP wiki.

# These settings apply to all clients. You can group clients, or target specific machines with specific settings: http://wiki.ltsp.org/wiki/Configuration

# This fixes a bug with Intel GPUs. Uncomment if your thin clients use them.

# Sets default screen to the "kiosk" screen script

# Sets kiosk executable to Google Chrome

# Suppresses first run dialogs in Chrome, especially the default browser prompt.

# Enable profile detection (if you set that up in the step above)

Configure DHCP Options

(Optional) DHCP for Virtual Testbeds

If you are setting up the virtual testbed as described above, here’s how to set up networking. One virtual interface (eth0) will be bridged to your real local LAN, and a second virtual interface (eth1) will both serve DHCP to thin clients, as well as NAT web traffic for clients. Warning: be sure to choose a subnet for thin clients that is different from your real LAN subnet!

  • Enable IP forwarding
    sudo vim /etc/sysctl.conf

    • Either add or uncomment this line:
    • To apply immediately
      sudo sysctl -w net.ipv4.ip_forward=1
  • Enable NAT
    sudo iptables --table nat --append POSTROUTING --jump MASQUERADE --source
  • Save these settings
    sudo sh -c 'iptables-save > /etc/ltsp/nat'
  • Configure network interfaces
    sudo vim /etc/network/interfaces

    # The loopback network interface
    auto lo
    iface lo inet loopback
    # The primary network interface
    auto eth0
    iface eth0 inet dhcp # You may want a static in production
    # The secondary network interface
    auto eth1
    iface eth1 inet static
        # This should NOT be on the same subnet as your real LAN!
        up iptables-restore < /etc/ltsp/nat
  • Configure DHCP for LTSP clients
    sudo vim /etc/ltsp/dhcpd.conf

    # Default LTSP dhcpd.conf config file.
    # Choose a different subnet as your real LAN!
    subnet netmask {
        option domain-name "ltsp.example.org";
        option domain-name-servers;
        option broadcast-address;
        option routers;
        option subnet-mask;
        option root-path "/opt/ltsp/i386";
        if substring( option vendor-class-identifier, 0, 9 ) = "PXEClient" {
            filename "/ltsp/i386/pxelinux.0";
        } else {
            filename "/ltsp/i386/nbi.img";
  • Restart networking
    sudo /etc/init.d/networking restart
  • Restart the DHCP server
    sudo service isc-dhcp-server restart

DHCP for Production

Thin clients rely on DHCP options 66 and 67 to PXE boot properly. Depending on which DHCP server you use, these options are set differently—an exercise I leave up to the reader. If you already have a DHCP server within reach of thin clients, I recommend using that to serve options 66 and 67. Only testbeds and specialized cases should serve DHCP from the LTSP server.

The options should be

  • Option 66
    <hostname or IP of LTSP server>
  • Option 67


At this point, boot any PXE-capable client within reach of the DHCP server to test things out. For those with the virtual testbed, launch your thin client VM, which should be all set to pull DHCP from, and NAT all web traffic to, the LTSP server.

After about a 90-second bootstrapping process, you should be at a full-screen Chrome browser. Note: the last 30 seconds or so turns the screen blank, as Chrome is loaded into RAM. This is normal, and I haven’t found a way to avoid this.

Installing the SBAC / CAASPP Secure Browser

Any sysadmin in K12 will know about this new digital testing platform students are required to use. Since they release an official Linux client, cursory testing shows it also works well. Disclaimer: Although brief testing shows positive, be sure to run your own tests before using this system to administer the SBAC / CAASPP test for your students!

  • Log into the chroot
    sudo ltsp-chroot -ma i386
  • Download the Secure Browser package (check the official page for any updates)
    cd /tmp && wget http://ca.browsers.airast.org/wp-content/uploads/secureBrowsers/CASecureBrowser6.5-Linux.tar.bz2
  • Extract the Secure Browser package
    tar xjvf CASecureBrowser6.5-Linux.tar.bz2 -C /opt
  • Install dependencies
    apt-get install sox festival ttf-mscorefonts-installer
  • Log out of the chroot
  • Update the chroot image
    sudo ltsp-update-image i386
  • Update the lts.conf so clients now boot to the Secure Browser
    sudo vim /var/lib/tftpboot/ltsp/i386/lts.conf
  • Comment out the existing KIOSK_EXE line which points to Chrome, as well as the KIOSK_OPTIONS line. Then add

After that, clients should boot straight to the new Secure Browser. If you want to get tricky with your lts.conf, you can direct only certain clients based on MAC address to the Secure Browser—while keeping all others on Chrome. Nice!

Raspberry Pi Support

My use case focused on bringing extra ROI in from an existing hardware investment. However, the workstations are bulky, power-hungry and, well, old. Raspberry Pi is an ultra-minimal, ultra-cheap hardware platform that, if paired with LTSP, would allow for extremely cheap hardware outlays.

Since RPi runs on an ARM CPU, the above process needs some adjustment. It turns out, one of the LTSP developers has released a brief guide for building an ARM-compatible client chroot, tailored specifically to LTSP.

However, the official Chrome package is not released for ARM (only i386 and AMD64), so we’ll need to use Chromium. The differences between official Chrome and open-source Chromium are minimal, but may require some tweaking to get full multimedia support.

Once I build and test the RPi chroot, I’ll update here with the results.


  • Closing Chrome with Alt+F4 or the upper-right X will reinitialize the kiosk session and start from scratch. This is great for when users are done, since this is all that’s necessary to erase any sessions or history and get to a clean slate for the next user.
  • This whole paradigm probably works with any single executable—it doesn’t just have to be Chrome. If you prefer Firefox, you can simply change lts.conf to KIOSK_EXE="/usr/bin/firefox" after installing it, of course. Note: changes to lts.conf do not require a client image rebuild!
  • Any change within the client chroot requires an image rebuild with ltsp-update-image i386. With typical resources, this takes about 3-5 minutes. The advantage of this approach is that squashfs over NBD is much more efficient than NFS, putting less strain on the server.
  • Be sure to update both the server and the client chroot regularly with sudo apt-get update && sudo apt-get upgrade
  • Restarting the server’s nbd-server service, or rebooting the server, will cause booted clients to crash. Keep this in mind when planning maintenance windows.


Thanks reddit for all your feedback and support. I really appreciate it, keep it coming!

I would like to thank the developers of LTSP, Debian and Ubuntu. Special thanks to Vagrant Cascadian (vagrantc) in #ltsp on irc.freenode.net for helping me get through some really mucky parts.


Creative Commons License
This work by Austin de Coup-Crank (ternarybit) is licensed under a Creative Commons Attribution 4.0 International License.
Based on a work at http://ternarybit.org/chrome-web-kiosk-guide/.

3 thoughts on “Turning old computers into Chrome web kiosks with LTSP”

  1. Great post!

    It’s a pleasure to see how simple something can become after binding together all the little pieces FLOSS provides.

    And you are the one opening this way for others. Thank you for sharing this doc!

  2. Excellent post. I have everything working except the chrome settings are not applying on the client (I don’t need separate configs for different networks). I made the 00-default.json file and updated the image, however, it just opens to the chrome default sign-in site. Any insight would be appreciated.


    1. Glad you liked it!

      The policy file is very picky. Try running it through a JSON linter for correct syntax. Also be sure you have it in the right place: /etc/opt/chrome/policies/managed and that it is readable by all users.

Leave a Reply

Your email address will not be published. Required fields are marked *