Only allow ssh key login on Raspberry Pi

Having ssh open on a raspberry Pi can be dangerous. To make it more secure we will setup SSH keys and disallow passwords.

This is the followup of Opening a Raspberry Pi to the outside world On the internet bots constantly try to hack servers. The Raspberry Pi is popular target. That’s why by default ssh is disabled nowadays.
To make this more secure we will only allow login by ssh key instead of a password. Ssh keys are a lot safer and you also won’t need to type/paste the password on login.

⚠️ Always test in a new terminal tab/window.
This guide has the potential to log yourself out of your pi.
If you always keep one loggedin window open you can always revert everything there.

Generate ssh keys

Generate keys on the Raspberry Pi: ssh-keygen -t rsa -b 4096 -C "pi-webserver"
Just press enter for both the location and password.
This key can be used for git authentication, log in to other machines and more. You might never need them, but it never hurts.

Add your own ssh key

On your own local machine also generate ssh keys. Check this github guide.

Once you’ve your own ssh keys. login on the pi and open File: nano ~/.ssh/authorized_keys

Paste your public key here and save.
For linux/mac you can find your public key with: cat ~/.ssh/

Open a new terminal window and try to login.

Disable Passwords

Open file: sudo nano /etc/ssh/sshd_config

Look for PasswordAuthentication
remove the # at the front and change the value to no.

PasswordAuthentication no

As a fall-back we will allow login in from the home network. So at the bottom of the file add. (the 4 spaces in front of the line matter)

Match address 192.168.*.*
    PasswordAuthentication yes

Restart the ssh service. sudo service ssh restart

Try to login in a new window. you should not need to enter a password.
If that works try login in outside of your home network easiest is probably to connect a smartphone tethering.

Raspberry Pi basic installation

This guide will start help you set up a Raspberry Pi with Raspberry Pi OS (Rasbian). After that, some basic configuration and updates.

This guide will help you set up a Raspberry Pi with Rasbian. Ready to do pretty much whatever you want.

What hardware do you need?

a Raspberry Pi, 4B a case, a network cable, power supply and an SD-card.
The Required Hardware
  • First off a Raspberry Pi of course, this tutorial uses a 4B 4GB. And should also work on a Model 2B and 3B.
  • Second I have a network cable. Wifi is available on since the 3B but wire always has a more stable connection, I will use a wire.
  • A USB-C power source. This can get a bit tricky. Recommended is at least 2A with 5V. A normal smartphone adapter won’t give that. Using less will slow down your Raspberry. If you’re using a 2B or 3B you need micro USB instead of USB-c
  • A micro SD card, at least class 10 for speed and with 8GB or more. The SD card will serve as the main harddrive for the pi. An micro SD adapter is also needed to install the image on the SD card.
  • A Case. With the Pi4 I’d recommend one with a builtin fan, as it can get to hot for itself.

Installing Rasberry Pi OS Lite

In the past this was called Rasbian. I liked that name much better….
I also have a guide for installing Ubuntu on the Pi.

This guide will start help you set up a Raspberry Pi with Rasbian. And basic Pi configuration.

The main screen of the Raspberry Pi Imager application
Raspberry Pi imager
Selecting Raspberry Pi OS Lite with the Pi Imager
Select Raspberry Pi OS Lite
Progress of writing the image with the Raspberry Pi Imager
Almost done writing the image.

Flashing the image can take a while. You could set up all the wires and put things in place.

After the SD card is done

When the writing of the SD card is done. Create a folder named ssh in the root of the boot partition.

Also in the boot partition is a file called cmdline.txt. Open that, and at the end of the line add the IP you want to give the Pi. Usually you’re restricted to 192.168.1.x or 192.168.2.x but that depends on your router settings.


If you need wifi place a file named wpa_supplicant.conf also in the boot partition. And add the following:


I haven’t tried this myself so more information here, The static IP probably won’t work with wifi.

Then plug in the SD card and powerup. Do wait 2~5minutes before actually trying. The Raspberry has to setup stuff for the first time. If after 20max you still can’t find an IP. The flashing of the SD card has gone wrong and you’ll have to restart.

Go to your own computer and open the terminal (or Putty for the windows users) Login with the command ssh pi@192.168.*.* Use the IP address you noted before. If it asks if you want to continue choose yes. and use the default password: raspberry Putty again will work a bit different.

Login in on a Raspberry Pi by a terminal
Logging in for the first time.

Global configuration

Now you are logged-in remote on the raspberry pi. Time to configure some stuff with the command sudo raspi-config.
On this menu we can configure some basic settings. Let’s change some settings.

the start screen of raspi-config tool
Start screen of the raspi-config screen.
  1. Change User Password CHANGE THE PASSWORD. Longer is better and safer
  2. Network options
    N1 Hostname the name of your Raspberry in your network. I named mine pi-webserver. Not required. It’s just a label. But useful if you got multiple Raspberry’s running.
  3. Boot options
    B1 Desktop / CLI Chose the ‘Console Autologin’ or ‘Console’ option. Don’t choose auto login if someone else might have physical access to the pi. Desktop will just waste power.
    B2 Wait for Network at Boot Just turn this off to be safe.
  4. Localisation Options Here you can change the timezone to your current one. You could change the language of the PI and the keyboard layout if you need to. Keep in mind I will be using English.
  5. Interfacing Options we don’t need this
  6. Overclock Only for the Pi 2B, Set this to the highest setting. It won’t hurt and will make the Pi a faster.
  7. Advanced Options This will be a bit more.
    A1 Expend Filesystem Run this. No need to reboot immediately.
    A3 Memory Split Set this to 16. Because we don’t have a interface we won’t need memory for that.
  8. Update Run this is should not hurt. It’s only to update the raspi-config tool.

When this is finished the pi might download some language packs. It it doesn’t reboot. you have to do it yourself. sudo reboot now.

Wait about a minute and login in again like before. But with the new password


This might take a while but a fresh install it’s highly recommended to update.

sudo apt update && sudo apt upgrade -y

Now we update all default installed software. If the image is old or hasn’t been updated in a while it might take a long time. The date of the image is in the file name of the downloaded image.
At times It might appear to hang. It Unless it’s stuck on one thing 15 minutes it’s probably fine. Give it an 15 to 30 minutes. Else pull the plug.

When it’s done reboot again just to be sure: sudo reboot now

You now have a Raspberry Pi ready for use.

Shadow Posttype

The case I was dealing with. There is a posttype “person” and it had the normal templates, a archive-person.php and single-person.php with a bunch of metadata.
Which where visible on and the archive
So far nothing special.

Every “person” was a -well- person. Some persons where also a “Judge”. A judge was an extra metabox on the person edit page.
And it needed it’s own pages on the frontend. and

Adding the rewrites

function judge_rewrite_rule() {
	 * Single judge
	 * set is_judge to 1
	 * set person to post slug
	add_judge_rewrite_rule( '^judge/([^/]+)/?', 'index.php?is_judge=1&person=$matches[1]', 'top' );

	 * Archive for judges
	add_rewrite_rule( '^judges/?', 'index.php?is_judge=1&post_type=person', 'top' );
add_action( 'init','rewrite_rule' );

function judge_rewrite_tag() {
	 * make the query_vars aware of `is_judge`
	add_rewrite_tag( '%is_judge%', '([0-9]+)' );
add_action( 'init', 'judge_rewrite_tag' );

The main 2 functions to look out for are add_rewrite_rule and add_rewrite_tag.
I register the query_var is_judge and set it when the url is /judges/ or /judge/john-doe

After adding this do flush the permalinks. Just go to the settings for permalinks and press save. No need to change anything.

Registering the templates

 * Set the correct theme/template-file.php
 * the shadow single & archive.
 * @param string $template
 * @return string
function assign_judge_template( $template ) {
	if ( 1 !== (int) get_query_var( 'is_judge' ) ) {
		return $template; // we only check for templates when the judge set.

	// assign the correct template.
	if ( is_single() ) {
		return get_template_directory() . '/single-person-judge.php';
	if ( is_post_type_archive( 'person' ) ) {
		return get_template_directory() . '/archive-person-judge.php';

	return $template; // fallback.
add_filter( 'template_include', 'assign_judge_template', 10, 1 );

When the is_judge is set set a different template. In this case this these templates are in the theme folder. You could also set templates from a plugin. The naming is can be anything but I picked single-person-judge.php so the next person can easier find the relation to the post type.

Setting the correct posts for the judge archive

Now the single will work as you would expect. But the archive page will still include all persons, not just the judges. Lets fix that.

 * @param WP_Query $query
function judge_pre_get_posts( $query ) {
	if ( 1 !== (int) $query->get( 'is_judge' ) || $query->is_single() ) {
		// only do this check if `is_judge` is set.
		// for single's this check is not needed.
	// the meta_query
	$meta_is_judge = [
	    'relation' => 'AND',
		[ 'key'     => 'is_judge', 'compare' => 'EXISTS',],
            'key'     => 'is_judge',
            'value'   => '1',
            'compare' => '=',

	// This part is to make sure you don't override other possible existing meta_queries
	$existing_meta   = $query->get( 'meta_query' );
	if ( empty( $existing_meta ) ) {
		$query->set( 'meta_query', $meta_is_judge );
	} else {
		$query->set( 'meta_query', [ 'relation' => 'AND', $existing_meta, $meta_is_judge, ] );


add_action( 'pre_get_posts', 'judge_pre_get_posts' , 10, 1 );

If you’re familiar with the pre_get_posts hook this should not be to hard.
We select all posts that have the post_meta is_judge set to 1.
The way I add the meta_query seems a bit excessive, but this makes sure you never override a possible existing meta_query which an other filter might have added.

Now every Judge should be listed on and every individual judge should be visible on Only thing left to do is link to the page.

Linking to the judge url

As we want a person to be visible on both and we can’t override the default permalink. So the best next thing is to create a helper function for calling the judge link.
This function should be used where you need to link to, like on the judge archive page.

 * Helper function
 * @param int|WP_Post|null $post
 * @return string, if not a judge, return normal permalink
function get_judge_permalink( $post = null ) {
	$post = get_post( $post );
	if ( is_null( $post ) ) {
		return null;

	// if the meta `is_judge` is not set, return the default person permalink.
	// for your use cases you might want to return something different.
	if ( '1' !== get_post_meta($post->ID, 'is_judge', true ) ) {
		return get_the_permalink( $post );

	return home_url( 'judge/' . $post->post_name );

The function can be used in the same way as the regular get_the_permalink. get_judge_permalink(); the post argument is optional, and can also be a post_id.

Closing thoughts

In the template files, I suggest adding a very clear comment at the top, which explains where the rewrite functions are. An other person will otherwise have a hard time to find where this is created.

The list of judges will now automatically appear on the /judge/ archive page. If you need to create this list on a custom WP_query just add the query_var is_judge, example:

$q_args = array(  
	'post_type' => 'person',
	'is_judge' => 1, // this will get picked up by the `judge_pre_get_posts` filter
	// other arguments
$custom_query = new WP_Query($q_args);
// do what you want

Install WordPress on Raspberry Pi

WordPress the CMS that doesn’t need introduction.In this guide we are going to install it and configure it a bit.
It is required you took the previous steps of setting up the Raspberry, installed php, nginx and mysql.

WordPress the CMS that doesn’t need introduction.In this guide we are going to install it and configure it a bit.

It is required you took the previous steps of setting up the Raspberry, installed php, nginx and mysql.

Setup Url

So far we’ve setup example.local we will create a second url and add configure it. I will use wordpress.local but feel free to use whatever you want.

First lets setup the correct directories.

sudo mkdir -p /var/www/wordpress.local/public_html
sudo chown www-data:www-data /var/www/wordpress.local -R
sudo nginx -t && sudo service nginx reload

Create the vhost and fill it with with the following.
sudo nano /etc/nginx/sites-enabled/wordpress.local.conf

server {
    listen 80;
    ## Your website name goes here.
    server_name wordpress.local www.wordpress.local;
    ## Your only path reference.
    root /var/www/wordpress.local/public_html;
    ## This should be in your http block and if it is, it's not needed here.
    index index.php;

    location = /favicon.ico {
            log_not_found off;
            access_log off;

    location = /robots.txt {
            allow all;
            log_not_found off;
            access_log off;

    location / {
            # This is cool because no php is touched for static content.
            # include the "?$args" part so non-default permalinks doesn't break when using query string
            try_files $uri $uri/ /index.php?$args;

    location ~* .(js|css|png|jpg|jpeg|gif|ico)$ {
            expires max;
            log_not_found off;
    location ~ .php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;
        fastcgi_intercept_errors on;
        fastcgi_buffers 16 16k;
        fastcgi_buffer_size 32k;

This config is the bare minimum as recommended by the nginx manual.
It works but it is not the best. The official WordPress docs has some more examples.
The main thing to improve is security.
I might do a better config in the future but for now this is all.

For the final step reload nginx.

sudo nginx -t && sudo service nginx reload

Create DB

Login to the mysql prompt:

sudo mysql -uroot

After that run the following lines to create a database, create a user with a strong password and assign permissions.

CREATE DATABASE raspimain_db;
CREATE USER 'raspimain_user'@'localhost' IDENTIFIED BY 'P@SSW0RD_Str0nG_R@nD0m_&_L0ng';
GRANT ALL PRIVILEGES ON `raspimain_db`.* TO `raspimain_user`@`localhost`;

Download WordPress

Go to the new WordPress directory.

cd /var/www/wordpress.local/public_html/

Download the latest version of WordPress, and unzip it.

sudo wget
sudo unzip
sudo mv wordpress/* ./
sudo rm -r wordpress
sudo chown www-data:www-data . -R

Install WordPress

A few things to check after the install is done.

Gravity Forms give editors access

By default Gravity forms isn’t accessible by editors. There are ways to do this with plugins like members.
As some might guess I prefer wp-cli for this:

wp cap add editor gravityforms_create_form gravityforms_edit_forms gravityforms_view_entries gravityforms_export_entries gravityforms_delete_entries graviyforms_delete_forms gravityforms_edit_entries gravityforms_view_entry_notes gravityforms_edit_entry_notes --grant
Difference in rights after setting capabilities

Running this will pretty much give all rights except the settings of Gravifty Forms.
It will allow creatign, editing & deleting forms. But also view entries and export them.

Full list of Gravity Forms Capabilities

Nano Shortcuts

I’ve know about nano and some it’s shortcuts. Today I explored them a bit more deeply.
So a list of shortcuts I find useful:

  • ctrl+K Cut the current line and put it in the nano clipboard (it’s not the same as the general clipboard)
  • ctrl+U Paste the line
  • ctrl+W Open search, type and hit enter. For the next match press alt+W
  • ctrl+Q Search backwards. For the next backward match press alt+Q
  • alt+U Undo
  • alt-E Redo
  • alt+C show the line number
  • alt-G Go to line number



PHP namespaces grouped

Consider the following code:

namespace abc;
use function is_string;
use function get_class;

class main {
    protected $string;
    public function __construct( $string ) {
        if ( is_string( $string )) {
            $this->string = $string;
        } else {
            $this->string = get_class( $this );

Nothing fancy. Just a class in a namespace using two functions from the global namespace (is_string & get_class).
Those two functions are imported from the global namespace as that will give a small performance boost.

But if you have 20-30 build in PHP functions that list will get very long….

Luckily you can merge them:

use function is_string, get_class;

For now I’m not sure I’ll always import build in PHP functions, the boost is small. And it’s annoying to keep track of.

sumcheck a whole directory

For some reason files changed on a server. Site down, always fun.
Restored a backup all good. This site did not have git on the server. But I still wanted to monitor the files for changes.

The one I landed on was:

find ./ -type f -name "*.php" -not -path "./wp-content/cache/*" -exec md5sum {} + | sort -k 2 | md5sum

Let’s dissect

What does this command do step by step

In the current directory and sub directory, list all files (not directories)

find ./ -type f

Limit it to php files

find ./ -type f -name "*.php"

Exclude the files in the caching directory, a bit weird syntax but it’s the one.

find ./ -type f -name "*.php" -not -path "./wp-content/cache/*"

For each file found run the command md5sum making a sum per file.

find ./ -type f -name "*.php" -not -path "./wp-content/cache/*" -exec md5sum {} +

Next we sort the output based on filepath+name.
We sort because find might return file order inconsistently.

find ./ -type f -name "*.php" -not -path "./wp-content/cache/*" -exec md5sum {} + | sort -k 2

Finally we create the grand total sumcheck based on all other sumchecks.

find ./ -type f -name "*.php" -not -path "./wp-content/cache/*" -exec md5sum {} + | sort -k 2 | md5sum



WordPress the_date skipping same days

Lets take a look at this very basic loop.

$query_name = new WP_Query();

if ( $query_name->have_posts() ) :
    while ($query_name->have_posts()): $query_name->the_post();

Nothing special right? Will by default just display the date, title and content of the first 10 posts.
But if 2 posts are published on the same day it will skip display that posts date.

When looking at the source code of the_date it compares the date of the previous post with the current using is_new_day.
I guess it can make sense in some scenario’s but too me it’s a bit weird by default.

To prevent this just use

echo get_the_date();