Set a flatpak env variable and run a CLI call to grant permissions. Jump down to “the fix” at the bottom to see how or read on for a little backstory!
The quest
For a while now I’ve been really loving daily driving Bluefin on both my laptop and my desktop. However, one of my biggest complaints, coming from running Xorg, is that my very most favorite screenshot utility doesn’t work on the desktop: Flameshot. Easily installed via a flatpak, it either errors out prompting on permissions or looks like it’s gonna launch but doesn’t. Strangely, I found It Just Works™ on my laptop which led me to think it was multi-monitor issue as my desktop is three up affair ( | | — if you must know).
Every now and again I try the work-arounds and they never work. Then today I was doing some work which required arrows on screenshots, the absolute happiest of paths for Flameshot. I thought I’d fumble through with some of the work-arounds to see if any of them worked.
Surprise surprise, I made two changes and it works! I’ll keep this post updated, because some times things change and it breaks, but as of right now, it just works works. Here’s a screenshot with like a bazillion arrows (remember, it works!! happy path!!) with some relevant versions:
(for the SEO types, thats: Gnome 47, OS Build 839d9ce, Bluefin (Version: gts-41.20250421 / FROM Fedora Silverblue 41) and Bluefin (Version: gts-41.20250421 / FROM Fedora Silverblue 41), Flameshot v12.1.0 () Compiled with Qt 5.15.15)
For some time I’ve had all my home lab systems running on LXD. For me at least, LXD predated Docker and so I’ve stuck with it. The containers are a bit more pet like and less cattle like – but that’s OK. They’re there for me to learn with – plus I can totes do docker in LXD inception if I wanna! (I’ll figure my way over to Incus some other month)
So years and years ago I found out bout FreshRSS as a great way to self host an RSS reader. This meant I didn’t have to install any apps on my phone, my feeds could stay synced and since I already had a VPN setup, it was trivial to access while on the road. Lovin’ it! I’d set this up around late 2018.
Fast forward to yesterday, I flippantly upgraded FresshRSS to the latest, as I occasionally do, and the site started having a fatal error, as it often does after I upgrade ;) I pulled up the error logs on the FressRSS LXD container running Apache and MariaDB and immediately saw some sort of unexpected { character in function blah blah on line 1-oh-smang-thirty error. Huh, what’s that about?
Turns out in the latest dev release of FreshRSS, they’d formally removed support for PHP 7.x and required PHP >8.0. Specifically, this was because they’re using Union Types in their function declarations. This is cool stuff! you can see the int|string values in the sample function here:
This is no problem! I’ll just update PH….P…. oh yeah – I’m on hella old Ubuntu 18…so then I’ll just find some third party apt repo and add that and then…. Hrrmm…might be more trouble than it’s worth. That’s cool! I’ll just deploy a new Ubuntu container on 24.04. Oh, well, that’s gonna take a chunk of disk space – I’ve been using a bit of Alpine to build small Docker images at work, what about a pet Alpine in LXD? They have an image – why not!
Preparing the leave the old system
Before we sudo rm -rf / on the old box (er, container), let’s get our data outa there. We need to first make a dump of the database. We’re root so we can just zing right through any permissions with a one liner. Next up we can zip up our old files into one big ol’ honkin zip file. That looks like this:
mysqldump freshrss > ~/freshrss.sql
cd /var/www/localhost/htdocs/
zip -r ~/freshrss.zip .
Finally, we can generate an SSH key for this root user to easily copy to the new container – I knowingly didn’t add a password because I’m about to delete the container and we’re all friends here:
ssh-keygen -t ed25519
cat /root/.ssh/id_ed25519.pub
Ok – I’ll hold on to that pub key for the “One last trip to the old digs” section below.
Bless the new hotness
Here’s how to bloop out an Alpine container in LXD:
lxc launch images:alpine/3.20 freshrss
lxc shell freshrss
That’s it! You’re now sitting as root on the new instance. Let’s install the base packages including Apache, MariaDB, OpenSSH and PHP with all it’s libraries:
As final hurrah at the old server, now that our SSH key is on the new server, let’s copy over the zip archive and the SQL dump. Be sure to replace 192.168.68.217 with your real IP!
scp ~/freshrss.* 192.168.68.217:
Go SQL!
Now that we have our server with all the software installed and all the data copied over, we just need to pull together all the correct configs. First, let’s run setup for MariaDB and then harden it. Note that it’s called mysql… , but that’s just for backwards compatibility:
rc-service mariadb setup
rc-service mariadb start
mysql_secure_installation
That last command will ask questions – default answers are all good! And the initial password is empty, so you can just hit return when prompted for the current password. Maybe check out passphraseme if you need a password generation tool? Let’s add the database, user and perms now. Be sure to not use password as your password though!
echo "CREATE USER 'freshrss'@'localhost' IDENTIFIED BY 'password';"| mysql
echo "GRANT ALL PRIVILEGES ON *.* TO 'freshrss'@'localhost'WITH GRANT OPTIO"| mysql
echo "GRANT ALL PRIVILEGES ON *.* TO 'freshrss'@'localhost'WITH GRANT OPTION;"| mysql
Now we can load up the SQL and move all the PHP files to their correct home. Again, we’re root so no SQL password, and again, this is actually MariaDB, not MySQL:
mysql freshrss < freshrss.sql
unzip freshrss.zip
mv * /var/www/localhost/htdocs/.
mv .* /var/www/localhost/htdocs/.
Go Apache!
Apache just needs four updates – easy!
Edit /etc/apache2/httpd.conf with your favorite editor. Find these three lines and uncomment them – they won’t be next to each other:
Now find the one line where DocumentRoot is set and change it to this value and add two more lines. One to allow encoded slashes and one to set the server name. Be sure to use the IP address or FQDN of the server – don’t use rss.plip.com!
DocumentRoot "/var/www/localhost/htdocs/p"
AllowEncodedSlashes On
ServerName rss.plip.com:80
Now that apache has been configured, let’s restart it so all the settings are loaded:
rc-service apache2 restart
Conclusion
The old FreshRSS install should now be running on your new Alpine based container – congrats! This has been a fun adventure to appreciate how Alpine works as compared to Ubuntu. This really came down to two main differences:
systemd vs OpenRC – Ubuntu has usedsystemd for sometime now and the primary interface to that is systemctl. Alpine on the other hand usesOpenRC which you interface with rc-update and rc-service. Alpine picked this up from when it split off from Gentoo.
apt vs apk – Package management is slightly different! I found this to be an inconsequential change.
There’s plenty of guides out there that do the same as this one. Heck, you’re likely better of just using a pre-built docker image (though the topresults were pinned to PHP7)! However, I wanted to document this for myself and hopefully I’ll save someone a bunch of little trips off to this wiki or that FAQ to understand how to migrate off of Ubuntu to Alpine.
A friend of mine created some fun stickers for use at the most recent DEF CON. They were sly commentary about how corporate a lot of the stickers are and how maybe we should get back to our DIY roots. But…what’s this? There’s a .xyz in there…is that a TLD…is there domain I could go to?! IS THIS STICKER AN AD ITSELF?!?!?!?!1!
It’s all of those things and none of those things – that’s why I love it so much. Best of all, when you go to website, you get just what you deserve ;)
The website was initially setup on a free hosting provider, but they didn’t provide any logs – something my friend was curious about to see how much traffic the non-ad ad was generating. I have a VERY cheap VPS server that already had Ubuntu server and Caddy on it, and I figured I could help by hosting a wee single file static web site and be able to easily offer the logs. Let’s see what we can do!
Step 1: One HTML file + Four Caddy config lines = Web server
I frickin’ love Caddy! I made a single index.html file and then added these 4 lines of config:
the-domain-goes-here.xyz {
root * /var/www/the-domain-goes-here.xyz
file_server
}
After I restarted Caddy (systemctl restart caddy) – I was all set! As DNS had already been changed to point to the IP of my lil’ server, Caddy auto-provisioned a free Let’s Encrypt cert, redirected all traffic from port 80 -> 443 and the site worked perfectly!
By default Caddy has logs turned off – let’s fix that!
Step 2: Turn up the (log) volume
Unsurprisingly, Caddy makes enabling logs very straight forward. Just add these three lines
I reloaded the config in Caddy (systemctl reload caddy) and checked for log activity in /var/log/caddy/. It was there! Oh…it was there in full repetitive, verbose JSON…OK, cool, I guess that’s a sane default in our new cloud init all JSON/YAML all the time world. However, how about common log format though?
This was the first time Caddy surprised me! While it was easy enough to do (huge props to “keen” on Stack Overflow), it was a bit more convoluted and verbose than I expected. You have to change the log deceleration to be log access-formatted and then specify both a format and a transform. The final full server config looks like this:
format transform `{request>remote_ip} - {request>user_id}[{ts}]"{request>method}{request>uri}{request>proto}"{status}{size}"{request>headers>Referer>[0]}""{request>headers>User-Agent>[0]}""host:{request>host}"` {
time_format "02/Jan/2006:15:04:05 -0700"
}
}
}
Now let’s figure how to to add secure access to download those logs.
Step 3: Rsync via Authorized Keys FTW!
A straight forward way to give access to the logs would be to create a new user (adduser username) and then update the user to be able to read the files created by the Caddy process by adding them to the right group (usermod -a -G caddy username). This indeed worked well enough, but it also gave the user a full shell account on the web server. While they’re a friend and I trust them, I also wanted see if there was a more secure way of granting access.
From prior projects, I knew you could force an SSH session to immediately execute a command upon login, and only that command, by prepending this to the entry in the authorized_keyfile:
If I had SOME_COMMAND be /usr/bin/rsync then this would be great! The user could easily sync the updates to their access log file at /var/log/caddy/the-domain-goes-here.xyz-access.log. but then I realized they could also rsync off ANY file that they had read access too. That’s not ideal.
The final piece to this Simple Single Page Site with Secure Log Access is rrsync. This is a python script developed specifically for the use case of allowing users to rsync only specific files via the Authorized Keys trick. The full array of security flags now looks like this:
As there’s no other logs in /var/log/caddy – this works great! The user just needs to call:
rsync -axv username@the-domain-goes-here.xyz: .
Because of the magic of rrsync (two rs) on the server forcing them into a specific remote directory, the rsync (one r) on client is none the wiser and happily syncs away.
Happy web serving and secure log access syncing and Happy Halloween!
tl;dr – Timekpr-Next Remote is an easy to use web app to add or remove time to users of linux login time tracking app, Timekpr-nExT
Recently, one of my kids got a drawing tablet and wanted to use it with Krita. Given they were on a Chromebook, we decided to repurpose an old Intel NUC i3 server with a clean Ubuntu 22.04 Desktop. Not only would this allow Krita to run well, but it would also enable video editing via KDEnlive and other hard-to-run on ChromeOS softwares.
For some time now, we’ve been happily running Legba to track computer usage. However, we wanted something with a bit more teeth, so we settled on Timekpr-nExT (henceforth just “Timekpr”, but it’s the “nExT” one, not this out of date one or this waaay out of date one, k?). This is a great app that allows for a finite amount of time to be used per day, and it is relatively easy to add more time. Well, easy if you’re on a desktop. And you have SSH installed. And you know the login and password to each computer you want to control. So, not at all easy if you’re a busy parent who’s juggling managing kids and helping with school work and cooking dinner. So, not at all easy if you’re a parent, amiright?!
Enter Timekpr-Next Remote! This is a Dockerized Python app that allows you to easily update update your kids’ computer time right from the nearest parental phone or desktop device:
As you can see, for any given user (only one sample user, “Muhammad”, is shown here) you can easily add more time (or remove time if you fat fingered the add time). Given how ubiquitous phones are, having a self hosted, non-cloud way to easily control time has been a win for us. Video chat with grandma after you’ve done your homework and used all your time allotment? Add -> 30 min -> Save, it only takes 3 seconds \o/
SSH FTW
Let’s have a look under the covers at how all this works.
I should start out by saying that Timekpr is licensed GNU GPL v3 and they post the code online. I did consider adding an HTTP server to handle REST requests to core package up stream. Then I realized I’d have to do the securely and that I’d have to deal with certificates and such. A greenfield approach would be quicker (grok only my code, not someone else’s) and more secure, but not as clean (I used SSH vs REST). With that out of the way…
Timekpr Remote uses SSH to communicate! This is clunky, indeed. But, Python has a wonderful SSH library in the form of Fabric (which turn uses the awesome Paramiko) which took a lot of the really clunky parts out and made them pretty elegant.
Here’s the flow of data when we load the page and want to get the current usage of Mumammad:
All data flows this way, and there’s three AJAX endpoints which the web client sends via this flow:
Get all Users and IPs
Get usage for a user and IP pair
Add/Remove time for a user and IP pair
This isn’t a perfect REST API, but it’s OK enough. It was my first time writing an app in Flask, so it was fun to figure how to do different URL handling and JSON returning and such, even if my REST uses some GETs instead of POSTs/PUTs
“s” in Timekpr-next Remote is for “Security”
While we an trust SSH between Timekpr Remote and Server, you may note a lack of authentication between the mobile handset and Timekpr Remote. Indeed, there is none. Here’s what I recommend:
Run something like Traefik or Caddy in another docker container. From there you can bind the timekpr-next remote server to the host docker IP with something like TIMEKPR_IP=172.17.0.1 docker compose up -d. It will no longer be available on the network, only only via the revers proxy you set up.
You can then either use basicauth (eg in Caddy) or what I did is make a host name that is un-guessable like https://user-time-8957446623432192758492038.domain.com. Everyone just bookmarks this. Even if your kids see the URL, they won’t be able to remember it.
For those that give SSH the stink eye (smells like an injection attack, eh?), you can harden this too. Ensure the SSH user on the server can not do anything more than run timekpra by restricting it in the authorized_keys file on each client. This will ensure if extra variables are passed (though we do explicitly protect against this), they won’t do any more harm.
Like and Subscribe that video
Here’s a 9 second video demonstrating it in situ!
Video Player
Media error: Format(s) not supported or source(s) not found
Hey hey! I’d been meaning to consolidate some of my VMs I’m paying for and semi-accidentally deleted the place where I was hosting my work’s instance of Keys-To-The-Tunnel (KTTT). With this VM deletion, I decided to leverage my recent dabblings with Caddy and do a big refactor of KTTT by replacing Apache with Caddy.
As a reminder, KTTT is an easy way for an organization which both develops web apps and uses GitHub to share their local dev app on the internet via a publicly accessible URL all protected with solid TLS and SSH encryption.
Let’s dive into what the update means and why I did it!
Parenthetical aside about TLS for local Android development
Originally I asked if potential KTTT users :
ever need to test an Android application against a web server such that you need a valid TLS certificate?
This is actually a bad idea if all you need is a valid TLS cert for Android testing. The reason is that you’ll literally be holding your phone in your hand which is less than a foot from the computer hosting your app you want to test against. By introducing KTTT into the mix, you send your traffic thousands of miles/kilometers (you pick which) and back just to get a TLS cert. Crazy times!
A much better approach is to use something like local-ip.co . If you want an easy way to keep your traffic entirely local, you can use nginx-local-ip with a one line docker compose call to set up everything you need to locally run a local-ip.co TLS. It’s a really sweet set up! (I’m biased because I help author some of it ;)
KTTT is still a great idea if you need to share your local dev environment though!
With that out of the way, back to KTTT updates…
What’s new with KTTT
For an end user of KTTT who just wants to share their app, it’s now WAY easier to figure out which SSH command to run and which URL to share. This is thanks to the handy web app which walks you through three easy questions:
For the administrators of KTTT, you’ll note that KTTT now uses Caddy instead of Apache. While there’s nothing wrong with Apache, Caddy is a simpler take on the needs of an app like KTTT that requires a bunch of small reverse proxies. Caddy is 7 years old and came to being in the world of Docker, containers and micro-services. Where as Apache is 27 years old and came being near the birth of world wide web.
Ironically, a key feature of Caddy, the ability to automatically provision and renew TLS certs, is NOT being used. Instead, the opportunity to use a wildcard TLS cert came up via acme-dns.io and I took it.
That all said, it’s a joy to use Caddy because I can create a simple JSON file with four lines to define a reverse proxy:
The web app above gives a pretty good idea of the improvements, but since I added a demo video of the whole KTTT experience on GitHub, may as well post it here in case you’re curious (44 second video):
Other odds and ends in 1.1.0
There’s also a bunch of other fixes and improvments I made while in there. Here’s the notes from the 1.1.0 release:
I’d been meaning to learn how to write an app using something more than CSV files, but less than MariaDB, to store files – I’m thinking SQLite of course! Then along came the desire to have a simple way to track when a computer was on a network as a proxy for kids’ daily screen time. After all, the network is the computer, right?
While there’s so very many ways to solve detecting if a computer is online (more on this later), I thought it’d be fun to write a simple app that could correlate multiple IPs to a single person, and then give a histogram of minutes per day per person. Given this is just a proxy for screen time, it’s fine if it doesn’t have alerting, password protection or even a way to prevent going over the allotted time per day. The goal will be for any interested parties to see how long a device has been on for the current day. It’s then up to the family to have a discussion about what it means to go over your daily allotment.
Ok, let’s do this! We have a requirement to track computers being online and to write and read the results to a SQLite DB. I’ve been groovin’ on learning Python, so let’s double down and use that. I did some Wikipedia exploring and read about Papa Legba, and thought it made a mighty fine sounding name. Finally, after some nudging from a friend, we’ll package it up in Docker so it’s easy to try out and host in an isolated container.
Ping FTW
The first step to using Legba, is to define a list of users and which IPs they’ll be on. Very likely the best way to do this is to either use static IPs on your LAN clients, or have your DHCP server set the same IPs per MAC every time.
Then you’ll create a conf.py file copied from the conf.example.py file and fill it out. Here we see Jon and Habib have one IP each, where as Mohamed has 2:
trackme = {
'Jon': ["192.168.1.82"],
'Habib': ["192.168.1.12"],
'Mohamed, ': ["192.168.1.240", "192.168.1.17"]
}
The code to track if a device is achieved via the subprocess module via a ping() function with just two lines that send a single ICMP packet:
# thanks https://stackoverflow.com/a/10402323
def ping(host):
""" Ping a host on the network. Returns boolean """
Back in the main() function, we then read in config, loop over each person and try and ping() each of their IPs. If we see them online, we write to the DB via record(). It ended up, just as I’d hoped, that Python’s SQLite libraries are robust and it’s just 6 lines to insert a row:
sql = ''' INSERT INTO status(name,state,date) VALUES(?,?,?) '''
cur = sqlite.cursor()
activity = (name, state, datetime.now())
cur.execute(sql, activity)
sqlite.commit()
return cur.lastrowid
Just before the end of the loop we call probably the most complex function of the lot output_stats_html(). This function is responsible for reading the day’s active users, getting each users activity by hour, the total for the day and finally output static HTML as well as a static JSON file that will get called via AJAX so the stats will auto-refresh.
At the end of the loop we sleep for 60 seconds. In theory if you had hundreds (thousands?!) of IPs to track and they were on connections with >500ms latency, it would take way longer than 60 seconds. Legba will not scale to this level. It’s currently been comfortably tested with 5-10 devices on a LAN where each device has ~20ms of latency.
A histogram is worth a 1000 words
After you’ve done a bit of a git clone with a lil pip3 install and fleshed out your own config.py and done a little systemd love, you’ll have some sweet sweet histograms! (Some keen eyed readers may note this histogram looks familiar ;)
It’s interesting to note that mobile devices, as seen withe “Adnon Cell”, are effectively on all the time. In this sense, Legba is not much use to track a cell phone. Meanwhile, Bobby Table’s desktop, Adnon’s Laptop and Chang’s Nintendo Switch all work as expected (NB – I didn’t actually test with a Switch).
Existing Solutions
I’ve been running this for solution for just about 4 months now. It’s been a great way for our family to have an open discussion about what it means to spend too much time on the computer and it’s been rock solid. Checking ls and select count(*) from status; I see my DB is 23MB and has 487,069 rows.
Given the simplicity of this app, could this DB and rows be easily stored and retrieved elsewhere? When I wrote the app, I didn’t care – I just wanted to write it for the fun of writing it! However, I was listening to episode 171 of Late Night Linux and they mentioned how utilitarian Telegraf is. It struck me that, indeed, if you had a Telegraf, InfluxDB and Grafana (aka “the TIG stack”) already set up, it would be pretty trivial to capture these same stats. I would do this by setting up a centralized instance of Telegraf and either use the built in Ping plugin, or possibly the more extensible [[inputs.exec]] input type. With the latter, you could even re-use parts of Legba to pretty trivially input the data to InfluxDB. Then, it would be equally trivial, to slice up the ping counts per hour, per user and have a slick dashboard. Just food for thought!
Otherwise, I hope some else than me gives Legba a try!
At work I’ve been doing a lot of Docker based projects. I ended up writing a neat little Bash utility which I then recently extended into what I’m calling DockStat. It shows running containers and their related resources. You could use it if you’re repeatedly upping, downing and destroying docker containers over and over like I was. Or maybe you just want a nice little dashboard to see what’s running on your server?
DockStat at work
However, if you have more than say a dozen active containers, this script might not scale nicely (oh, perhaps a monitor in portrait mode might fix this? ;)
Being the good little open source nerd that I am, this is of course available for download with a permissive license in the hopes that some one will find it useful or possibly even offer a PR with some improvements to my nascent Bash coding skills.
Background
With countless primers on how to use Docker out there, I won’t get into what the commands all mean, but the impetus for this script was repeatedly running docker ps to show a list of the active containers. A bit later I remembered you could run endless Bash loops with a one-liner which made the process a bit nicer as it auto-refreshed:
whiletrue; do clear;date;docker ps;sleep 5; done
A bit after that I stumbled upon the glorious watch command! Wow – just when you think that that you know and OS, they come and show you that there’s this awesome command they’ve been hiding from you all these years. Thanks Linux!
watch greatly improved on my Bash one-liner as it was an even short one-liner, could trivially be configured to refresh at what ever frequency you wanted and show a header or not. The icing on the cake was that prevents the flash of a redraw upon refresh:
watch -t -n 1 docker ps
About now I got more cozy with the --format feature built into most Docker command line calls. This was handy because I could reduce the number of fields shown in docker ps that I wasn’t interested in. Here’s maybe the simplest of them which shows JUST the container name and if how long the it has been running:
docker ps --format='{{.Names}}{{.Status}}'
Research continued on how to architect the helper script. I had need to show different data than docker ps had to offer. I branched out into docker inspect as well as finding other Dockeristas one-liners that I shamelessly co-opted (I’d be honored if anyone did the same of my work!!). This allowed me to joined ps and inspect like as seen with this fave that shows all the running containers’ and their internal IPs:
I was ready to assemble all the docker Data dukets I’d gathered into a nice CLI dashboard so our app developers could see the status of our containers booting. Finding a solution for this enabled both the the helper script to spring forth and simultaneously created the nascent DockStat. This was a Bash utility that was both easy to use, automates flash-less refreshes and introduced basic terminal layout functionality with a near zero learning curve (assuming you know Bash): Bash Simple Curses
Thanks
Thanks to James and Russ for reviewing my code and an early draft of this post. I’ve been trying to improve both my posts and code and this won’t happen without folks kind donation of their time and input!
Back when I was writing Cattrotar on a Raspberry Pi (or really, any other of the myriad times I was hacking on a small embedded device), I often faced some sort of problem or another:
I thought I set up the network for a device, but can no longer remotely access it over WiFi
I’ve just written a new OS to a microSD card, want to further configure the OS but it’s not yet reachable on the network
I want to take my set up to the hacker space to be social while I work on an issue
I’m waiting in the library for my kids to finish up a class and can’t easily get my Pi on the library WiFi to hack on it from my laptop
I don’t want to carry around, or even bother setting up, a keyboard and monitor to get access to my Pi
The Solution
As many of you lovely readers may know, I’m already a fan of small travel routers made by GL.iNet, like the fine GL-MT300N-V2. For a scant $20 dollars (shipped!) you get DHCP server you can not only bring with you, but will happily run off a battery. Better yet, because it has a USB port, you can actually power your Pi off the wee router. Daisy chain USB power for the win! The final icing on the cake is that you can make the two Ethernet ports on be both LAN (instead of LAN & WAN). While these are great devices, their WiFi stack is a bit flaky, so being able to hardwire your network devices is a great, if not slightly cumbersome, work around.
The net result is that your around town bag can have:
Pi (In the picture below I have an Orange Pi Zero with a temp sensor and a screen threaded through one side of a case)
When you put it all together, you get a nice tidy mess and it works just great, solving all of the above problems. Your laptop can be on two networks so it can have local access to the Pi and to the internet too. Further, if the WiFi your on isn’t too hostile with their captive portals and such, you can actually have the GL.iNet router act as a WiFi repeater and backhaul that bandwidth to your Pi. Run your apt update away!
This set up is not only small and lightweight, but lets me work un-tethered on my Pi setup and Python code. Now if this frickin pandemic would just be solved, I’d actually be able to take this out of the house instead of just co-working with my partner in our office in our house!
13 years ago we got an iPod to actually listen to music on the go. It was awesome! Some time later we had our first kid and some time after that smart phones became prevalent, so our iPod fell out of use. But it was about 9 years ago that we started to use the old iPod as an easy way to play the same a bedtime playlist for our kids (oh yeah, we had a 2nd kid at some point too ;). Soon, the kids split into their own bedrooms so we picked up a used iPod on eBay and loaded up the same bedtime playlist onto it. These two iPods dutifully played the same songs day in and day out every night for years. They’re even more utilitarian than designed, but worked well at their finite task (Photo by Nicnicoleleeolee):
However, over time things started to not work. First one of the fancy docks we used as a speaker stopped charging which ever iPod was in it. This meant every week or so we’d have to swap it over to the good dock to charge up. But then one started to lock up and had to be rebooted. We feared we’d have to replace them soon.
It was around this time that I starting using the Cast All The Things software (aka catt) in my quest to both (finally) learn Python and to easily control the volume on a chromecast. Checkout my cattmate and cattrotar projects! catt is a command line script and python library that allows you to easily control and play videos/music on Chromecasts. It was also around the time that Chomecast Audio’s were stopped being made so I’d stocked up them. We have about 4 or 5 plugged in here and there about the house, including one in each kids room. They work really well!
By now you can likely see how this post is going to resolve itself, but lets find out, shall we?
Yes, that’s right, I wrote a full featured web site that you could pull up and easily play music in which ever room needed to hear the play list. It was simple, yet fancy ;) On the front end there was a series of 2 buttons, one for each room. When you pressed a button, it sent an AJAX call to a to the single endpoint on the back end. The back end then made an exec call to a bash script. This in turn used catt to play a MP3 on the specified Chromecast.
This worked great for a day or so. But then a bug reared it’s ugley had. Let me tell you, the cost of a software bug in your web app which involves waking your child up at 10.30pm is EXTREMELY high. Like, unacceptably high. Sleeping children are gold. Parents get grown up time, the kids get much needed rest and we’re all better for it the next day. Don’t fuck with a kid’s sleep.
After some unfruitful debugging, I got lazy and realized I’d already installed the wonderful termux on my phone, along with a $2 add on to have a bash script launcher widget on my phone’s desktop. So, after a dozen or so minutes of coding and a little ssh-keygen -t ed25519for good measure, I had this on my phone:
Here’s what happens when you press one of those four links:
Call a local script on the phone with the same name shown above
Each script has the same contents but just calls the remote server with a different argument: ssh napserver controlmusic.sh CHILD PLAY_STOP. So for the first one above, that’d be ssh napserver controlmusic.sh e play
The remote server, (hardened of course to only allowing one command to be run for that SSH key,) inside controlmusic.sh runs a catt command: /usr/bin/catt -d DEVICE COMMAND OPTIONAL_COMMAND. Again for the first button that’d look like: /usr/bin/catt -d "E's Chromecast" cast songs.mp3
So, while not nearly has fancy as the web app I initially wrote, it works every time, has saved me the time of debugging the web app (aka let me be lazy ;) and most importantly, does not wake the kids up after they’ve gone to sleep! Icing on the cake is that continued my lazy streak and bought the app for my partner’s phone so they could activate the playlist while I’m out of town (instead of me VPNing in and activating it remotely on request ;).
Like all of you parents lucky enough to still have a job during the COVID19 layoffs, I’ve been struggling to balance time at work, personal time, family time and being the family’s IT person. With school closed, and now all summer camps closed, our use of kids screen time (aka internet time) has gone up from 0.5hrs/day to 3hrs+/day. How do we ensure we have safe computing environment for them?
DoT by Design
Originally, we had a MacOS workstation for the kids with parental controls enabled. This allowed us to do things like set up a 30 min per day limit, create separate accounts for each kid, limit which apps they could use and, most importantly, limit which URLs they could use (deny all, allow some). When coupled with my love of LXD/Pi-Hole/Quad9, that looked like this:
In this scenario the kid’s single shared workstation would get an IP lease from the DHCP server running on the pfSense router. This lease would specify the house wide Pi-Hole which sent all it’s DNS clear text queries to Stubby which in turn sent them encrypted to Quad9 via DNS over TLS (DoT). This is really nice as not only do we do get LAN wide ad blocking, but we get LAN wide encrypted DNS too. Score!
The kids workstation gets no special treatment on the network and is a peer of every other DNS lease on the LAN. However, with them needing to do school work and have fun and learn over the summer, they’ve since each gotten their own workstations. Now we have three workstations! This is starting to be a hassle to maintain the lock down on which sites they can browse. As a result, we just told them “be good” and let them use their new workstations with out any filters. This is sub-optimal!
.* Blacklist
How can we improve this situation to make it more tenable and more secure? By adding more instances of Pi-Hole, of course! It’s trivial to add a new instance of Pi-Hole with LXD. Just add a new container lxc launch ubuntu: pi-hole2 and then install Pi-Hole on the new container with curl -sSL https://install.pi-hole.net | bash . It’s two one liners that take all of 5 minutes.
For those of you like me that want an easy way to export their existing whitelist from MacOS’s parental controls, check out the “Directory Service command line utility” aka dscl. With this command you can create a file with all the URLs you’ve whitelisted. You can then easily import them into your new Pi-Hole instance (be sure to swap out USERNAME for your user):
Back to the new Pi-Hole instance, if we set the upstream DNS server to be the initial Pi-Hole, this means the kids DNS gets all the benefits of the existing encrypted infrastructure, but can add their own layer of blocking. Here I configure their Pi-Hole to just use the existing Pi-Hole as the resolver:
Specifically, if you add .* as a blacklist, EVERY site on the internet will fail to resolve. Then you can incrementally add sites you want resolve to your whitelist:
Once we hard code each of the three workstations to use the new Kids DNS, we’re good to go! And, this indeed works, but the savvy technologist will see the time suck of a flaw in my plan: If you whitelist example.com, there’s 5 or more sites you need to whitelist as well in order for example.com to work. This is because 99% of all sites use 3rd party javascript via content delivery networks (CDNs), have integrations with social media and of course often use the ever present Google Analytics. It gets even more tricky because if you want to keep your kid from searching on Google, you can’t think, “Oh, I’ll just whitelist *.google.com and then all it’ll save a bunch of time!”. Along with that will come Gmail and who knows what ever else. I knew this issue would be there going in, so I wasn’t afraid to take the time to get it to work. But caveat emptor!
Teaching Kids to be Smart
Speaking of caveats of a plan – all parents should know that this plan is VERY easy to bypass once your kids starts to figure out how the internet and their specific devices work. I’ve literally told my kids what I’m doing (stopping just about every site from working) why I’m doing it (the internet can be a horrible place) and that they can likely figure a way around it (see Troy Hunt’s tweet – as well as his larger write up on parenting online).
Like Troy Hunt, I’ll be super proud when they figure a way around it – and that day will come! But I do want to prevent them from randomly clicking a link and ending up somewhere we don’t want them to be. They can then ask us parents about why they can’t access a site or when it might allowed.
Being honest with your kids about what you’re doing is the way for them to be aware that this is for their benefit. The end goal is not to lock the entire internet away forever, it’s actually the opposite. The end goal is to prepare them to be trusted with unfettered access to the internet. This will happen soon enough whether we parents want it or not!
Banning 8.8.8.8 et al.
While I was in there tuning up the DNS, I remembered that some clients on my network (I’m looking at you Roku!) weren’t listening to the DHCP rules about using my preferred, encrypted DNS and going direct to Google’s DNS (8.8.8.8) or others I didn’t like. After a little research I found I could redirect all outbound TCP and UDP DNS traffic so that all devices use my Pi-Hole/Stubby/Quad9 DNS* whether they thought they were or not. For others running pfSense and want to do this, see the steps to “Blocking DNS Queries to External Resolvers” and then “Redirecting all DNS Requests to pfSense” (both thanks to this Reddit thread).
Some of you may be thinking, “this seems like a lot of work, why don’t you just implement an existing off the shelf solution?” Good question! For one, I like to DIY so I control my data and what’s done with it instead of letting a 3rd party control it. As well, while there’s home based solutions, I prefer open source solutions. To put my money where my mouth is, I’ve just donated for the 2nd (3rd?) time to Pi-Hole. I encourage you to do the same!
To be clear though, this set up is a pretty crude tool to achieve the end result. It looks like there’s some quite polished solutions out there if you’re OK with closed source, cloud hosted solutions. As well, there’s of course other variations on the “Use Pi-Hole For Parental Controls“.
Wrapping Up
Now that we have all in this in place, we can trivially support N clients which we want to force to use the kids more lock down DNS set up. This looks like exactly like it did before, but we have an extra container in the LXD server (and, some what orthogonally, a fancier pfSense DNS blocking setup):
I suspect this set up won’t last for more than a year or two. As more and more sites get added to the white list, it will be harder and harder to maintain. Maybe after that I’ll give each kid their own Pi-Hole instance to run on an actual Raspberry Pi and let them do with it as they please ;)
(Of course just after I deployed this, Pi-Hole 5.0 came out which offer the concept of groups, so you can likely do this idea above in a single instance instead of multiple. A bummer for me now, but a win for all other Pi-Hole users, including my future use!)