<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[saleshorse blog]]></title><description><![CDATA[saleshorse blog]]></description><link>https://blog.saleshorse.org</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1762608188865/1cf19d3b-678e-4434-89e7-da7554a826aa.png</url><title>saleshorse blog</title><link>https://blog.saleshorse.org</link></image><generator>RSS for Node</generator><lastBuildDate>Sat, 16 May 2026 20:10:50 GMT</lastBuildDate><atom:link href="https://blog.saleshorse.org/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[On Side Projects]]></title><description><![CDATA[The idea of a side project needs a bit of reframing.
I used to think they needed to be this formal thing. I'd set up a Trello board, a Notion page, write a business plan in some cases.
But those projects ended up feeling too much like work for me to ...]]></description><link>https://blog.saleshorse.org/on-side-projects</link><guid isPermaLink="true">https://blog.saleshorse.org/on-side-projects</guid><category><![CDATA[side project]]></category><category><![CDATA[goals]]></category><category><![CDATA[Beginner Developers]]></category><category><![CDATA[beginner]]></category><category><![CDATA[beginnersguide]]></category><category><![CDATA[project management]]></category><dc:creator><![CDATA[Sharif Elkassed]]></dc:creator><pubDate>Tue, 09 Apr 2024 04:42:36 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/S7mAngnWV1A/upload/4040f4765dc9e3dde7a447ad9c7ee519.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>The idea of a side project needs a bit of reframing.</p>
<p>I used to think they needed to be this formal thing. I'd set up a Trello board, a Notion page, write a business plan in some cases.</p>
<p>But those projects ended up feeling too much like work for me to enjoy them, and I'd always end up ditching them as as result.</p>
<p>There's nothing wrong with ditching a side project, but ditching a side project that you invested "real world" expectations into feels kinda shitty.</p>
<p>After all, what's the point of a side project?</p>
<p>That answer is probably a little bit different for everyone; it could be something for your portfolio. It could be to test out a new technology or framework. It could be something that you're looking to make money from.</p>
<p>Or it could just be for fun.</p>
<p>No matter the case, setting expectations for the project is key. The expectations should align with the overall goal of the project.</p>
<p>Most side projects should come with the expectation that they will be abandoned; the only exceptions are side projects for a portfolio when trying to get hired.</p>
<p>Side projects you're looking to make money from are not side projects. They are another class of application altogether, and thinking of them as side projects is detrimental.</p>
<p>Design your side project around the reason you're making it, not around what the project actually is. If you're making a movie app so you can learn Svelte -- focus more on how you can push your limits with Svelte, and less trying to make the best movie app.</p>
<p>Don't be afraid to take your time and explore. Take detours and scenic routes. Make U-turns with abandon. Get lost.</p>
<p>This is the very essence of the side project.</p>
]]></content:encoded></item><item><title><![CDATA[How to Run a Minecraft Server: Multiple Approaches]]></title><description><![CDATA[Background
In An intro to Minecraft mods, I outline my reasons for choosing the Fabric framework over Forge. Continuing with that same reasoning, this post will be outlining multiple approaches for running a Minecraft server using Fabric (although at...]]></description><link>https://blog.saleshorse.org/how-to-run-a-minecraft-server-multiple-approaches</link><guid isPermaLink="true">https://blog.saleshorse.org/how-to-run-a-minecraft-server-multiple-approaches</guid><category><![CDATA[minecraft]]></category><category><![CDATA[minecraft server]]></category><category><![CDATA[Docker]]></category><category><![CDATA[minecraft mods]]></category><category><![CDATA[Docker compose]]></category><dc:creator><![CDATA[Sharif Elkassed]]></dc:creator><pubDate>Tue, 27 Jun 2023 11:52:56 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/PYyPeCHonnc/upload/b4aa79fcae9bb622813024dcc3e8c126.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-background">Background</h2>
<p>In <a target="_blank" href="https://blog.saleshorse.org/an-intro-to-minecraft-mods#heading-fabric">An intro to Minecraft mods</a>, I outline my reasons for choosing the Fabric framework over Forge. Continuing with that same reasoning, this post will be outlining multiple approaches for running a Minecraft server using Fabric (although at a high level, the concepts should still apply to Forge).</p>
<p>You may want to do some research beforehand to determine which mods you want to run on your server, since Fabric mods will only run on a Fabric server, and Forge mods will only run on a Forge server.</p>
<p>If you're not sure what a Minecraft server is, you can simply think of it as a place to house Minecraft mods. To get a better idea of how a server fits into modding in general, read the <a target="_blank" href="https://blog.saleshorse.org/an-intro-to-minecraft-mods#heading-fabric-server">Fabric Server section</a> of the aforementioned post.</p>
<h2 id="heading-overview">Overview</h2>
<p>You might be wondering where the "multiple approaches" bit comes in, since we are only focusing on Fabric. We'll be covering the following ways to host a Fabric server:</p>
<ul>
<li><p>local machine</p>
<ul>
<li><p>Windows</p>
</li>
<li><p>Linux/MacOS</p>
</li>
</ul>
</li>
<li><p>VPS</p>
</li>
</ul>
<p>Regardless of where the server is hosted, it can be invoked (or "spun up") in multiple ways:</p>
<ul>
<li><p>raw commands in a shell</p>
</li>
<li><p>running a script file</p>
</li>
<li><p>running the server in a Docker container</p>
</li>
</ul>
<h2 id="heading-choosing-a-server-location">Choosing a server location</h2>
<p>In the <a target="_blank" href="https://blog.saleshorse.org/an-intro-to-minecraft-mods#heading-development-environment">Development Environment section</a> of the first post, we covered the fact that your development environment matters, in terms of which computer you use. Conversely, the computer you choose to use as your server doesn't matter at all -- so long as it has the proper version of Java and enough RAM to run Minecraft.</p>
<h3 id="heading-separate-machine-vs-local-machine">Separate machine vs local machine</h3>
<p>Speaking of RAM, that is one reason you might choose to run your server on a separate computer; the default amount of RAM allocated to the Minecraft server is 2GB, which your computer may not have to spare.</p>
<p>Even if RAM isn't an issue for you, you may still want a separate machine for other reasons -- a big one being availability; if you want your server to be accessible round-the-clock, you likely want a separate machine. Otherwise, your server goes down if your computer shuts off or loses network connectivity.</p>
<p>On the other hand, if your server needs are more casual and constant availability isn't a concern, then running the server locally (from your main machine) might be enough for you.</p>
<h3 id="heading-a-new-computer-isnt-necessary">A new computer isn't necessary</h3>
<p>You also might be thinking, "I can't afford to buy another computer to run a server on". If that's the case, you could explore these options:</p>
<ul>
<li><p>using an old computer</p>
</li>
<li><p>buying a Raspberry Pi (can be found for less than $100)</p>
</li>
<li><p>using a Virtual Private Server (monthly cost depending on usage and server specs)</p>
</li>
</ul>
<h3 id="heading-docker">Docker</h3>
<p>Docker might be a good option if you want to be able to reuse the same server configuration on multiple devices, or if you don't want to alter an existing Java configuration. Here are a few things to note about running a server in a Docker container:</p>
<ul>
<li><p>it will still consume RAM on whatever computer is running the container</p>
</li>
<li><p>no Java configuration is required</p>
<ul>
<li>Java is automatically installed in the container</li>
</ul>
</li>
<li><p>Docker containers don't "care" where they are run from</p>
<ul>
<li><p>you can bundle the container's configuration into a file on your local machine</p>
</li>
<li><p>that same file can be used to run an identical container on a separate machine (regardless of that machine's OS)</p>
</li>
</ul>
</li>
<li><p>Docker can run from</p>
<ul>
<li><p>your local machine</p>
</li>
<li><p>another physical machine</p>
<ul>
<li><p>an old laptop</p>
</li>
<li><p>a Raspberry Pi</p>
</li>
</ul>
</li>
<li><p>a Virtual Private Server</p>
</li>
<li><p>...or any other hardware that supports Docker.</p>
</li>
</ul>
</li>
</ul>
<p>Here's an example of when you might want to use Docker:</p>
<p>You are testing out a Minecraft server configuration with multiple mods, and multiple custom game settings. You're testing this configuration out on your laptop, but you want to be able to recreate the configuration on another machine without having to install each mod and enable each custom game setting all over again.</p>
<p>This is a great reason to use Docker. At a high level, the configuration would look something like this:</p>
<ul>
<li><p>find and download a Docker <em>image</em> that is preconfigured for running a Minecraft server</p>
<ul>
<li>think of the image as the blueprint for the container</li>
</ul>
</li>
<li><p>use the image to run a <em>container</em></p>
<ul>
<li><p>the image's blueprint is used to construct and "run" the container</p>
</li>
<li><p>think of a container as a self-contained environment for which to run software</p>
</li>
</ul>
</li>
<li><p>load the mods you want to test into the container via a <em>volume</em></p>
<ul>
<li><p>a volume is a way to link file storage between the <em>host machine</em> and the container</p>
<ul>
<li>the host machine is the computer that Docker is installed on</li>
</ul>
</li>
<li><p>the volume is where the <code>/mods</code> folder will live</p>
</li>
</ul>
</li>
<li><p>once you've decided on the game settings and mods you want to use, put instructions on how the container should be run in a <code>docker-compose.yml</code> file</p>
<ul>
<li><p>Docker Compose files describe the specifics of how the server container be run, such as</p>
<ul>
<li><p>the version of Minecraft to use</p>
</li>
<li><p>which user(s) to grant operator permissions</p>
</li>
<li><p>game settings (max players, spawn point, mobs, etc.)</p>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<p>We'll illustrate these concepts with examples later in the <a target="_blank" href="https://blog.saleshorse.org/how-to-run-a-minecraft-server-multiple-approaches#heading-running-a-server-with-docker">Running a Server with Docker</a> section.</p>
<h2 id="heading-running-a-server">Running a Server</h2>
<h3 id="heading-java-prerequisite">Java Prerequisite</h3>
<p>If you plan on running a server on a machine other than your own, make sure to install Java on that machine, following the <a target="_blank" href="https://fabricmc.net/wiki/player:tutorials:java:windows">instructions provided by Fabric</a>.</p>
<p>If you're using Docker, make sure the image you're using has the correct version of Java. The image likely has a way to specify a Java version with a <a target="_blank" href="https://docs.docker.com/engine/reference/commandline/tag/">tag</a> for the image.</p>
<p>If you're using Linux for your server, the instructions Fabric provides for setting up Java are fairly basic. For more detailed instructions, see <a target="_blank" href="https://blog.saleshorse.org/installing-java-on-ubuntu-without-breaking-your-shell">Installing Java on Ubuntu</a>.</p>
<h3 id="heading-loading-a-mod-onto-the-server">Loading a mod onto the server</h3>
<p>For mods to work on the server, we need to make sure that our Minecraft client has those same mods. More specifically, that means:</p>
<ul>
<li><p>you have an installation of the Fabric launcher (client) whose version matches the Minecraft version of your server</p>
<ul>
<li>this is the launcher you select when you open the Minecraft application on your computer</li>
</ul>
</li>
<li><p>the <code>/mods</code> folder for your Fabric launcher installation contains:</p>
<ul>
<li><p>the <code>.jar</code> file for the proper version of the Fabric API</p>
</li>
<li><p>a <code>.jar</code> file for each mod</p>
</li>
</ul>
</li>
<li><p>the <code>/mods</code> folder for your Fabric server must contain the same <code>.jar</code> files as the <code>/mods</code> folder in the Fabric launcher</p>
</li>
</ul>
<p>In other words, the <code>/mods</code> folder within your Fabric launcher <em>client</em> should have the <em>same contents</em> as the <code>/mods</code> folder on your Fabric <em>server</em>. If you want to read more about the reasoning, check out the <a target="_blank" href="https://fabricmc.net/wiki/tutorial:side">Fabric docs</a> on this topic.</p>
<ul>
<li><p>once the <code>/mods</code> folders match on the client and server, launch the server</p>
</li>
<li><p>once the server is running, grant <code>op</code> (operator) permissions to necessary player(s)</p>
</li>
<li><p>connect to the server using the <a target="_blank" href="https://blog.saleshorse.org/how-to-run-a-minecraft-server-multiple-approaches#heading-connecting-to-your-server">Connecting to your server</a> instructions below</p>
</li>
<li><p>verify that you can use the mod</p>
</li>
</ul>
<p>Approach-specific instructions are detailed below.</p>
<h3 id="heading-running-a-server-on-linuxmacos">Running a Server on Linux/MacOS</h3>
<p>This is perhaps the most accessible option since it can be run from:</p>
<ul>
<li><p>a machine running Linux</p>
<ul>
<li><p>Raspberry Pi</p>
</li>
<li><p>VPS</p>
</li>
</ul>
</li>
<li><p>a machine running MacOS</p>
</li>
</ul>
<p>Here we'll follow instructions from the <a target="_blank" href="https://fabricmc.net/wiki/player:tutorials:install_server">Fabric wiki</a>, and walk through them as we go.</p>
<p>If you've been following along, you should already have Java installed. The next step is to go to the <a target="_blank" href="https://fabricmc.net/use/server/">server download page</a> and download the relevant version:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># make a directory for the server</span>
mkdir fabric-server
<span class="hljs-built_in">cd</span> fabric-server

<span class="hljs-comment"># download the file to our fabric-server directory</span>
curl -OJ https://meta.fabricmc.net/v2/versions/loader/1.19.4/0.14.19/0.11.2/server/jar

<span class="hljs-comment"># confirm the download worked</span>
ls
<span class="hljs-comment"># output</span>
fabric-server-mc.1.19.4-loader.0.14.19-launcher.0.11.2.jar
</code></pre>
<p>Now that we have the server's <code>.jar</code> file downloaded, we can use the command supplied on the download page to launch the server using that <code>.jar</code> file:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># launch the server, allocating 2GB RAM</span>
java -Xmx2G -jar fabric-server-mc.1.19.4-loader.0.14.19-launcher.0.11.2.jar nogui
</code></pre>
<p>We get an error, which is expected because we need to accept the EULA:</p>
<pre><code class="lang-bash">[21:10:21] [main/ERROR]: Failed to load properties from file: server.properties
[21:10:21] [main/WARN]: Failed to load eula.txt
[21:10:21] [main/INFO]: You need to agree to the EULA <span class="hljs-keyword">in</span> order to run the server. Go to eula.txt <span class="hljs-keyword">for</span> more info.
</code></pre>
<p>If we inspect the contents of our server folder, we can see that files and directories have been generated as a result of attempting running the server. Among these is the file <code>eula.txt</code>:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># in fabric-server directory</span>
ls -la <span class="hljs-comment"># press enter</span>

<span class="hljs-comment"># output</span>
drwxr-xr-x  4 idev idev   4096 Jun 12 21:10 .fabric
-rw-r--r--  1 idev idev    158 Jun 12 21:10 eula.txt
-rw-r--r--  1 idev idev 155095 Jun 12 21:09 fabric-server-mc.1.19.4-loader.0.14.19-launcher.0.11.2.jar
drwxr-xr-x  8 idev idev   4096 Jun 12 21:10 libraries
drwxr-xr-x  2 idev idev   4096 Jun 12 21:10 logs
drwxr-xr-x  2 idev idev   4096 Jun 12 21:10 mods
-rw-r--r--  1 idev idev   1272 Jun 12 21:10 server.properties
drwxr-xr-x  3 idev idev   4096 Jun 12 21:10 versions
</code></pre>
<p>Now we can open that file with our preferred text editor and change <code>false</code> to <code>true</code>:</p>
<pre><code class="lang-bash">vim eula.txt

<span class="hljs-comment"># inside eula.txt</span>
eula=<span class="hljs-literal">true</span>
<span class="hljs-comment"># save and exit eula.txt</span>
</code></pre>
<p>Now that we've agreed to the EULA, we can re-run the command to launch the server:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># launch the server, allocating 2GB RAM</span>
java -Xmx2G -jar fabric-server-mc.1.19.4-loader.0.14.19-launcher.0.11.2.jar nogui
</code></pre>
<p>The server starts up this time with no errors:</p>
<pre><code class="lang-bash">[21:15:16] [Worker-Main-2/INFO]: Preparing spawn area: 91%
[21:15:16] [Worker-Main-2/INFO]: Preparing spawn area: 94%
[21:15:17] [Worker-Main-5/INFO]: Preparing spawn area: 97%
[21:15:17] [Server thread/INFO]: Time elapsed: 28858 ms
[21:15:17] [Server thread/INFO]: Done (35.063s)! For <span class="hljs-built_in">help</span>, <span class="hljs-built_in">type</span> <span class="hljs-string">"help"</span>
</code></pre>
<p>Now that the server is running, we can click into the terminal and type <code>op your_player_name</code></p>
<pre><code class="lang-bash">[14:24:08] [Server thread/INFO]: Time elapsed: 8357 ms
[14:24:08] [Server thread/INFO]: Done (10.491s)! For <span class="hljs-built_in">help</span>, <span class="hljs-built_in">type</span> <span class="hljs-string">"help"</span>
op awesomeminecraftplayer42023
[14:25:01] [Server thread/INFO]: Made maddoxchunk a server operator
</code></pre>
<h3 id="heading-running-a-server-on-windows">Running a Server on Windows</h3>
<p>The <a target="_blank" href="https://fabricmc.net/wiki/player:tutorials:server:windows">Fabric</a> instructions do a pretty good job of walking through this one, but we'll go through the steps here anyway (starting with <em>STEP 3: Install Fabric in the Server Folder</em>):</p>
<ul>
<li><p>go <a target="_blank" href="https://fabricmc.net/use/installer/">here</a> and click the 'download for Windows' button</p>
</li>
<li><p>run the file</p>
</li>
<li><p><strong>select the 'Server' tab</strong></p>
</li>
<li><p>select the version and install the location</p>
<ul>
<li><p>the Minecraft version should match the Minecraft version of the mod(s) you want to use</p>
</li>
<li><p>note that this file location has nothing to do with your installation of Minecraft</p>
</li>
</ul>
</li>
<li><p>press 'Install'</p>
</li>
<li><p>click 'Download server jar'</p>
</li>
<li><p>click 'Generate'</p>
</li>
<li><p>open the folder location where you saved the server to</p>
</li>
<li><p>run the <code>start.bat</code> file</p>
</li>
<li><p>you'll get an error about the EULA</p>
</li>
<li><p>close the terminal</p>
</li>
<li><p>open <code>eula.txt</code> in the server folder that has the <code>start.bat</code> file</p>
</li>
<li><p>change <code>false</code> to <code>true</code> in <code>eula.txt</code></p>
</li>
<li><p>save and exit the file</p>
</li>
<li><p>run <code>start.bat</code> again</p>
</li>
<li><p>click into the terminal with the server running and type <code>op your_player_name</code> to grant operator permissions</p>
</li>
</ul>
<h3 id="heading-running-a-server-with-docker">Running a Server with Docker</h3>
<p>I won't go over installing Docker, since their <a target="_blank" href="https://docs.docker.com/engine/install/">official docs</a> should be enough to get you there.</p>
<p>It's worth mentioning that once you have Docker installed, the steps below will work the same (more or less), regardless of your operating system.</p>
<h4 id="heading-run-a-vanilla-minecraft-server-in-a-container">Run a Vanilla Minecraft server in a container</h4>
<p>Once you've got Docker installed, it's time to download and run a Minecraft server image.</p>
<ul>
<li><p>start by going to the <a target="_blank" href="https://docker-minecraft-server.readthedocs.io/en/latest/">documentation for the Docker image</a> we're using</p>
</li>
<li><p>run the listed command</p>
<ul>
<li><p><code>docker run -d -it -p 25565:25565 -e EULA=TRUE itzg/minecraft-server</code></p>
</li>
<li><p>this runs the container in the background so that our terminal is free to run other commands</p>
</li>
<li><p>confirm the container is running with <code>docker ps</code></p>
</li>
</ul>
</li>
<li><p>copy the <code>CONTAINER ID</code> from the <code>docker ps</code> command</p>
</li>
<li><p>run <code>docker stop &lt;CONTAINER ID&gt;</code> to stop the container</p>
</li>
<li><p>run <code>docker ps</code> again to confirm the container is not running</p>
</li>
</ul>
<h4 id="heading-run-a-fabric-minecraft-server-in-a-container">Run a Fabric Minecraft server in a container</h4>
<p>The <code>docker</code> command above allowed us to run a "vanilla" Minecraft server. However, we are interested in running a Fabric server, so we consult the <a target="_blank" href="https://docker-minecraft-server.readthedocs.io/en/latest/types-and-platforms/">Types and platforms documentation</a>.</p>
<p>From here we can select 'Fabric' from the menu on the left. We can see a command that instructs us on how to run a Fabric server by using the <code>-e TYPE=FABRIC</code> flag. However, there is something else that's different about this command: it's mounting a volume with <code>-v /path/on/host:/data</code>. Before we run this command, we need to set up that directory on our host machine (the one running Docker).</p>
<h4 id="heading-mounting-a-data-directory">Mounting a data directory</h4>
<p>If we head to the <a target="_blank" href="https://docker-minecraft-server.readthedocs.io/en/latest/data-directory/">Data directory</a> documentation, we get some instructions on how to do this.</p>
<p>Let's try running a container with an attached data volume, using the supplied command from the <a target="_blank" href="https://docker-minecraft-server.readthedocs.io/en/latest/types-and-platforms/server-types/fabric/">Fabric section of the image docs</a>:</p>
<pre><code class="lang-bash">mkdir mc-data

docker run -d -v ./mc-data:/data \
    -e TYPE=FABRIC \
    -p 25565:25565 -e EULA=TRUE --name mc itzg/minecraft-server
</code></pre>
<p>Okay, so we've started a container and mounted the volume, but how do we use it?</p>
<p>It's a bit difficult to picture right now, since we're running the container as a background process with the <code>-d</code> flag. Let's try running it again without that flag and see what happens:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># first, remove/stop the container</span>
docker rm mc

<span class="hljs-comment"># run the same command without the -d flag</span>
docker run -v ./mc-data:/data \
    -e TYPE=FABRIC \
    -p 25565:25565 -e EULA=TRUE --name mc itzg/minecraft-server

<span class="hljs-comment"># output</span>
[init] Running as uid=1000 gid=1000 with /data as <span class="hljs-string">'drwxrwxr-x 2 1000 1000 4096 Jun 15 11:12 /data'</span>
... <span class="hljs-comment"># (more output)</span>
[init] ERROR: version lookup failed:
</code></pre>
<p>This isn't an error with the volume. We would get the same error if we ran the command without the <code>-v flag</code>. The only reason we didn't see the error before is that we ran the process in the background, so the output wasn't visible.</p>
<p>This error is because we need to specify a Fabric/Minecraft version for the container to run.</p>
<p>Let's try it again, this time using the appropriate flags to specify the version:</p>
<pre><code class="lang-bash">docker rm mc

docker run -v ./mc-data:/data \
    -e TYPE=FABRIC \
    -e VERSION=1.19.4 \
    -p 25565:25565 -e EULA=TRUE --name mc itzg/minecraft-server
</code></pre>
<p>We still get an error, but this time it's different:</p>
<pre><code class="lang-bash">[init] ERROR failed to install Fabric launcher from 1.19.4, LATEST, LATEST
</code></pre>
<p>This is yet another misleading error message. In my case, it was happening due to an issue with WSL2. The issue is resolved by deleting the <code>~/.docker/config.json</code> file.</p>
<p>If you encountered the error above, go ahead and delete the <code>config.json</code> file, then run the commands again to make sure it works:</p>
<pre><code class="lang-bash">docker rm mc

docker run -v ./mc-data:/data \
    -e TYPE=FABRIC \
    -e VERSION=1.19.4 \
    -p 25565:25565 -e EULA=TRUE --name mc itzg/minecraft-server
</code></pre>
<p>Now that we know we can get the server running, it's time to add our mod.</p>
<h4 id="heading-loading-mods-onto-the-server">Loading mods onto the server</h4>
<p>Looking again at the <a target="_blank" href="https://docker-minecraft-server.readthedocs.io/en/latest/data-directory/">Data directory documentation</a>, we can see that the <code>/mods</code> folder lives inside the <code>/data</code> directory, which we have created as <code>mc-data</code> on our host machine.</p>
<p>Speaking of the data directory, if we inspect the contents of <code>mc-data</code>, we can see that the folder is no longer empty since we have successfully run the server:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># stop the server if it's still running using CTRL + C</span>
ls mc-data

<span class="hljs-comment">#output</span>
banned-ips.json      fabric-server-mc.1.19.4-loader.0.14.21-launcher.0.11.2.jar  ops.json           whitelist.json
banned-players.json  libraries                                                   server.properties  world
crash-reports        logs                                                        usercache.json
eula.txt             mods                                                        versions
</code></pre>
<p>Recall from <a target="_blank" href="https://blog.saleshorse.org/how-to-run-a-minecraft-server-multiple-approaches?#heading-loading-a-mod-onto-the-server">Loading a mod onto the server</a> that the <code>/mods</code> folder on our server should match the <code>/mods</code> folder in our Fabric launcher installation. As such, we copy the contents of that folder to our data directory for the container</p>
<pre><code class="lang-bash"><span class="hljs-comment"># copy the contents of the Fabric </span>
<span class="hljs-comment"># launcher's /mods folder into the mc-data/mods folder</span>
cp -a /path/to/fabric/launcher/mods .mc-data/mods
</code></pre>
<p>Now we are ready to run our server again:</p>
<pre><code class="lang-bash">docker rm mc

docker run -v ./mc-data:/data \
    -e TYPE=FABRIC \
    -e VERSION=1.19.4 \
    -p 25565:25565 -e EULA=TRUE --name mc itzg/minecraft-server
</code></pre>
<p>Go ahead and connect to your server using the <a target="_blank" href="https://blog.saleshorse.org/how-to-run-a-minecraft-server-multiple-approaches#heading-connecting-to-your-server">instructions</a> below.</p>
<p>The game loads up when we connect, but we have a problem -- we aren't able to run commands. Normally, we could just go to our terminal and <code>op</code> our player, but things work a bit differently with Docker. Since we are running the server in a container, we don't have direct access to the terminal like we do when we run the server directly in the operating system.</p>
<p>So how do we issue commands to our Docker container? Enter <code>RCON</code>.</p>
<h4 id="heading-rcon">RCON</h4>
<p><code>RCON</code> is short for Remote Control, and allows Minecraft commands to be issued <em>remotely</em>.</p>
<p>Okay, but... how do we use it?</p>
<p>Thankfully, the Docker image we're using includes <code>rcon-cli</code>. Let's see it in action:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># stop the container (if running)</span>
docker stop mc

<span class="hljs-comment"># start the container again</span>
docker start mc
</code></pre>
<p>Before we can run <code>rcon-cli</code>, we need to wait for server to completely load, since <code>RCON</code> does not become enabled until that point. When you see the following message in the terminal, you're ready to use <code>rcon-cli</code>:</p>
<pre><code class="lang-bash">[03:28:23] [Server thread/INFO]: Done (8.096s)! For <span class="hljs-built_in">help</span>, <span class="hljs-built_in">type</span> <span class="hljs-string">"help"</span>
[03:28:23] [Server thread/INFO]: Starting remote control listener
[03:28:23] [Server thread/INFO]: Thread RCON Listener started
[03:28:23] [Server thread/INFO]: RCON running on 0.0.0.0:25575
</code></pre>
<p>Once the <code>RCON</code> service has started, we can open a new terminal window and use <code>rcon-cli</code>:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># replace your_player_name with the actual name of your player</span>
docker <span class="hljs-built_in">exec</span> mc rcon-cli op your_player_name

<span class="hljs-comment"># output</span>
Made your_player_name a server operator
</code></pre>
<p>Now that you are an operator, you can use commands in Minecraft. Go ahead and use the appropriate command(s) to test out your mod (if applicable).</p>
<h4 id="heading-docker-compose">Docker Compose</h4>
<p>When I originally explained the use cases for Docker, a big selling point was the portability, and the ability to reuse configurations. So far, we haven't seen how Docker can achieve that. We ran a bunch of commands, and even hit some errors in the process.</p>
<p>How can a workflow like that be reusable?</p>
<p>The answer is that it can't, and that's where Docker Compose comes into play.</p>
<p>Now that we have a <em>working</em> configuration, we can package that configuration into a <code>docker-compose.yml</code> file so that we don't have to remember the commands. Instead, we can just point Docker to the <code>docker-compose.yml</code> file.</p>
<p>Let's go through the <a target="_blank" href="https://docker-minecraft-server.readthedocs.io/en/latest/#using-docker-compose">instructions</a>:</p>
<ul>
<li><p>first install Docker Compose by following the <a target="_blank" href="https://docs.docker.com/compose/install/">instructions here</a>.</p>
</li>
<li><p>make a new directory and change into it</p>
<pre><code class="lang-bash">  mkdir mc-compose 
  <span class="hljs-built_in">cd</span> mc-compose
</code></pre>
</li>
<li><p>create a <code>docker-compose.yml</code> file and put the following in it</p>
</li>
</ul>
<blockquote>
<p>note that the file below is slightly different from the file linked in the documentation (we added the <code>VERSION</code> and <code>TYPE</code> environment variables)</p>
<p>also note that <strong>indentation matters</strong> in a <code>.yml</code> file</p>
</blockquote>
<ul>
<li><pre><code class="lang-bash">      version: <span class="hljs-string">"3.8"</span>

      services:
        mc:
          image: itzg/minecraft-server
          tty: <span class="hljs-literal">true</span>
          stdin_open: <span class="hljs-literal">true</span>
          ports:
            - <span class="hljs-string">"25565:25565"</span>
          environment:
            EULA: <span class="hljs-string">"TRUE"</span>
            VERSION: <span class="hljs-string">"1.19.4"</span>
            TYPE: <span class="hljs-string">"FABRIC"</span>
          volumes:
            <span class="hljs-comment"># attach the relative directory 'data' to the container's /data path</span>
            - ./data:/data
</code></pre>
</li>
</ul>
<h4 id="heading-using-an-env-file">Using an <code>.env</code> file</h4>
<p>To make things even more reusable, we can use an <code>.env</code> file located in the same folder as our <code>docker-compose.yml</code> file.</p>
<pre><code class="lang-bash">vim .env

<span class="hljs-comment"># .env</span>
VERSION=1.19.4
SERVER_TYPE=FABRIC
DATA_DIR=./mc-data
</code></pre>
<p>Now we can update our <code>docker-compose.yml</code>:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># docker-compose.yml</span>
version: <span class="hljs-string">"3.8"</span>

services:
  mc:
    image: itzg/minecraft-server
    container_name: mc
    tty: <span class="hljs-literal">true</span>
    stdin_open: <span class="hljs-literal">true</span>
    ports:
      - <span class="hljs-string">"25565:25565"</span>
    environment:
      EULA: <span class="hljs-string">"TRUE"</span>
      VERSION: <span class="hljs-string">"<span class="hljs-variable">${VERSION}</span>"</span>
      TYPE: <span class="hljs-string">"<span class="hljs-variable">${SERVER_TYPE}</span>"</span>
    volumes:
      <span class="hljs-comment"># attach the relative directory 'data' to the container's /data path</span>
      - <span class="hljs-string">"<span class="hljs-variable">${DATA_DIR}</span>:/data"</span>
</code></pre>
<p>Run the server with <code>docker compose up</code> to make sure the new config works.</p>
<h4 id="heading-automating-rcon">Automating RCON</h4>
<p>Remember how we had to wait for the server to spin up before we could use <code>rcon-cli</code>? That was kind of annoying. It would be nice if there were a way to automate this.</p>
<p>Thankfully, someone made a pull request, and some <a target="_blank" href="https://github.com/itzg/docker-minecraft-server/pull/1391">new commands were added</a> to the image. These commands allow <code>rcon-cli</code> to run automatically, either when the server starts, or when the game client connects to the server.</p>
<p>We'll keep things simple and just automate the provisioning of operator permissions. In the <code>docker-compose.yml</code> file below, you'll notice a new <code>RCON_CMDS_STARTUP</code> property under <code>environment</code>:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># docker-compose.yml</span>
version: <span class="hljs-string">"3.8"</span>

services:
  mc:
    image: itzg/minecraft-server
    container_name: mc
    tty: <span class="hljs-literal">true</span>
    stdin_open: <span class="hljs-literal">true</span>
    ports:
      - <span class="hljs-string">"25565:25565"</span>
    environment:
      EULA: <span class="hljs-string">"TRUE"</span>
      VERSION: <span class="hljs-string">"<span class="hljs-variable">${VERSION}</span>"</span>
      TYPE: <span class="hljs-string">"<span class="hljs-variable">${SERVER_TYPE}</span>"</span>
      RCON_CMDS_STARTUP:  |-
        /op your_username_goes_here
    volumes:
      <span class="hljs-comment"># attach the relative directory 'data' to the container's /data path</span>
      - <span class="hljs-string">"<span class="hljs-variable">${DATA_DIR}</span>:/data"</span>
</code></pre>
<p>Go ahead and run the container with <code>docker compose up</code>. If you get an error like <code>Additional property RCON_CMDS_STARTUP is not allowed</code>, the issue is likely that something isn't indented properly. Go back into the file and make sure the new command is indented one level in from <code>environment</code>.</p>
<p>But remember, we want this <code>docker-compose.yml</code> file to be as reusable as possible. Once it's complete, any changes that we need to make to the configuration should be done via the <code>.env</code> file. Let's put our player name into the <code>.env</code> file:</p>
<pre><code class="lang-bash">vim .env

<span class="hljs-comment"># .env</span>
VERSION=1.19.4
SERVER_TYPE=FABRIC
DATA_DIR=/mnt/c/Users/iDev/fabric-1.19.4-client/
OP_PLAYER=your_username_goes_here
</code></pre>
<p>Now we can adjust our <code>docker-compose.yml</code> file accordingly:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># docker-compose.yml</span>
version: <span class="hljs-string">"3.8"</span>

services:
  mc:
    image: itzg/minecraft-server
    container_name: mc
    tty: <span class="hljs-literal">true</span>
    stdin_open: <span class="hljs-literal">true</span>
    ports:
      - <span class="hljs-string">"25565:25565"</span>
    environment:
      EULA: <span class="hljs-string">"TRUE"</span>
      VERSION: <span class="hljs-string">"<span class="hljs-variable">${VERSION}</span>"</span>
      TYPE: <span class="hljs-string">"<span class="hljs-variable">${SERVER_TYPE}</span>"</span>
      RCON_CMDS_STARTUP:  |-
        /op <span class="hljs-variable">${OP_PLAYER}</span> <span class="hljs-comment"># note that there are no quotes in this command</span>
    volumes:
      <span class="hljs-comment"># attach the relative directory 'data' to the container's /data path</span>
      - <span class="hljs-string">"<span class="hljs-variable">${DATA_DIR}</span>:/data"</span>
</code></pre>
<p>Note that the syntax is slightly different for this command, compared to the others. There are no quotes around the command. Using quotes will cause an error.</p>
<p>It's also possible to specify more than one player, but the error handling still needs to be worked out. This means that if you make a typo in one player's name, or one of the players doesn't exist, the entire command will fail.</p>
<p>If you're interested in experimenting with this, the syntax for specifying multiple players is as follows:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># .env</span>
<span class="hljs-comment"># OP_PLAYERS=player1:player2:player3 # this will fail; no valid players</span>
OP_PLAYERS=your_user_name_goes_here:player2 <span class="hljs-comment"># this will fail; at least one invalid player</span>
</code></pre>
<h4 id="heading-reusing-a-docker-config">Reusing a Docker config</h4>
<p>So far, we've seen how using Docker Compose can save us from having to type long complicated commands. We can take it a step further and place our <code>docker-compose.yml</code> file on another machine so that it can run the same container with the same configuration.</p>
<p>First things first, we'll copy the file from our local machine over to a Virtual Private Server that has Docker installed. Also note that a user named <code>mc-admin</code> has already been created on the VPS:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># in mc-compose folder on local machine</span>
<span class="hljs-comment"># make sure to add the '/' at the end of vps-mc!</span>
rsync -a ./docker-compose.yml digitalocean.minecraft:/home/mc-admin/vps-mc/
</code></pre>
<p>If you're unfamiliar with <code>rsync</code>, here is what the command above does:</p>
<ul>
<li><p>takes the <code>docker-compose.yml</code> file in our current directory on the local machine</p>
</li>
<li><p>uses SSH behind the scenes to log into the VPS (<code>digitalocean.minecraft</code>)</p>
<ul>
<li><p>an SSH config file obscures the username and IP of the Digital Ocean VPS we're logging into</p>
</li>
<li><p>to execute without a SSH config file:</p>
<ul>
<li>replace <code>digitalocean.minecraft</code> with <code>user@ip.address.of.vps</code></li>
</ul>
</li>
</ul>
</li>
<li><p>creates a directory <code>vps-mc/</code> on the VPS, under the existing <code>mc-admin</code> user's <code>/home</code> directory</p>
<ul>
<li><p>this is why the <code>/</code> is important at the end of the command</p>
</li>
<li><p>without the <code>/</code>, <code>vps-mc</code> will be created as a file instead of a folder</p>
</li>
</ul>
</li>
<li><p>places the <code>docker-compose.yml</code> file in the <code>/home/mc-admin/vps-mc</code> directory of the VPS</p>
</li>
</ul>
<p>Since I've previously configured <code>ssh</code>, I don't need to supply a username and password when I run the command above.</p>
<p>If you haven't configured <code>ssh</code>, it would look more like this:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># we need to specify username@server</span>
rsync -a  ./docker-compose.yml mc-admin@docker.vps.ip:/home/mc-admin/vps-mc/
</code></pre>
<p>If you want to learn how to simply your <code>ssh</code> flow like I have, make sure to check out my <a target="_blank" href="https://blog.saleshorse.org/managing-multiple-ssh-keys-on-both-ends">Managing SSH Keys post</a>.</p>
<p>Before we run the server, we need to transfer over a few more files and folders:</p>
<pre><code class="lang-bash">rsync ./.env digitalocean.minecraft:/home/mc-admin/vps-mc/
rsync -a --mkpath ./mc-data/mods/ digitalocean.minecraft:/home/mc-admin/vps-mc/mc-data/mods/
</code></pre>
<p>Now we're ready to run the server on the VPS:</p>
<pre><code class="lang-bash">ssh digitalocean.minecraft

<span class="hljs-comment"># now in vps</span>
<span class="hljs-built_in">cd</span> vps-mc
docker compose up

<span class="hljs-comment"># output</span>
permission denied <span class="hljs-keyword">while</span> trying to connect to the Docker daemon socket at 
unix:///var/run/docker.sock: 
Get <span class="hljs-string">"http://%2Fvar%2Frun%2Fdocker.sock/v1.24/containers/json?all=1&amp;filters=%7B%22label%22%3A%7B%22com.docker.compose.config-hash%22%3Atrue%2C%22com.docker.compose.project%3Dpi-mc%22%3Atrue%7D%7D"</span>: 
dial unix /var/run/docker.sock: connect: permission denied
</code></pre>
<p>Okay, so we got a permissions error. No biggie, this is a pretty common thing to run into with Docker. I had already configured it on my local machine, but we need to set it up on the VPS.</p>
<h4 id="heading-enabling-rootless-docker">Enabling rootless Docker</h4>
<p>Yes, prefixing Docker Compose command with <code>sudo</code> would fix our issue, but running Docker as root goes against best practices, so we're going to do it the proper way:</p>
<p>You can follow along with this step by viewing the <a target="_blank" href="https://docs.docker.com/engine/security/rootless/#:~:text=Rootless%20Docker%20in%20Docker,%3A%2Ddind%20.&amp;text=The%20docker%3A%2D,root%20user%20(UID%201000).">Docker documentation</a>.</p>
<ul>
<li><p>first we'll start by updating our package manager</p>
<ul>
<li><code>sudo apt-get update &amp;&amp; sudo apt-get upgrade</code></li>
</ul>
</li>
<li><p>next we'll install the package that Docker tells us we need</p>
<ul>
<li><code>sudo apt-get install uidmap</code></li>
</ul>
</li>
<li><p>follow the instructions under <strong>Distribution-specific hint</strong></p>
<ul>
<li><p><code>sudo apt-get install -y dbus-user-session</code></p>
</li>
<li><p>the package is already installed, so no need to log out/back in</p>
</li>
</ul>
</li>
<li><p>follow the instructions under the <strong>Install</strong> section</p>
<ul>
<li><p><code>dockerd-rootless-setuptool.sh install</code></p>
</li>
<li><p>receive <code>command not found</code> error</p>
</li>
<li><p>continue following instructions: <code>sudo apt-get install -y docker-ce-rootless-extras</code></p>
</li>
<li><p>received another error: <code>Unable to locate package docker-ce-rootless-extras</code></p>
</li>
<li><p>switch to the <em>Without Packages</em> tab and run <code>curl -fsSL https://get.docker.com/rootless | sh</code></p>
</li>
</ul>
</li>
</ul>
<p>The installation was successful, but we have some output to read through:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># output from the install script</span>
[INFO] Installed docker.service successfully.
[INFO] To control docker.service, run: `systemctl --user (start|stop|restart) docker.service`
[INFO] To run docker.service on system startup, run: `sudo loginctl enable-linger sharif`

[INFO] Creating CLI context <span class="hljs-string">"rootless"</span>
Successfully created context <span class="hljs-string">"rootless"</span>
[INFO] Using CLI context <span class="hljs-string">"rootless"</span>
Current context is now <span class="hljs-string">"rootless"</span>

[INFO] Make sure the following environment variable(s) are <span class="hljs-built_in">set</span> (or add them to ~/.bashrc):
<span class="hljs-built_in">export</span> PATH=/usr/bin:<span class="hljs-variable">$PATH</span>

[INFO] Some applications may require the following environment variable too:
<span class="hljs-built_in">export</span> DOCKER_HOST=unix:///run/user/1000/docker.sock
</code></pre>
<p>Going through the sections of output one by one:</p>
<ul>
<li><p>since I want Docker to run when the system starts, I run the provided command</p>
<ul>
<li><code>sudo loginctl enable-linger sharif</code></li>
</ul>
</li>
<li><p>I need to set both the <code>PATH</code> and <code>DOCKER_HOST</code> environment variables</p>
<ul>
<li><p>I'm using <code>zsh</code> and not <code>bash</code>, so environment variables are set differently</p>
</li>
<li><p>I refer to a <a target="_blank" href="https://blog.saleshorse.org/installing-java-on-ubuntu-without-breaking-your-shell#shell-environment-table">previous blog post of mine</a> to set the environment variables at the profile level</p>
</li>
</ul>
</li>
</ul>
<p>Now we should be able to run <code>docker compose up</code> from our <code>vps-mc</code> directory again without getting a permissions error.</p>
<h2 id="heading-connecting-to-your-server">Connecting to your server</h2>
<p>On our computer with Minecraft installed, we can open our Minecraft Launcher, and confirm that we can connect to the server.</p>
<ul>
<li><p>select the Fabric Launcher installation</p>
<ul>
<li>make sure the version of Minecraft for the launcher aligns with the version of Minecraft for your server</li>
</ul>
</li>
<li><p>when the game loads, select 'Multiplayer'</p>
</li>
<li><p>accept the warning about third-party online play</p>
</li>
<li><p>select 'Direct Connection'</p>
</li>
<li><p>depending on how you set up your server, you'll need to enter the hostname accordingly</p>
<ul>
<li><p>local server: <code>localhost:25565</code></p>
</li>
<li><p>VPS: <code>&lt;IP of your server&gt;:25565</code></p>
</li>
</ul>
</li>
<li><p>click 'Join Server'</p>
</li>
</ul>
<h2 id="heading-resources">Resources</h2>
<ul>
<li><p><a target="_blank" href="https://nickjanetakis.com/blog/install-docker-in-wsl-2-without-docker-desktop">Install Docker in WSL2 without Docker Desktop</a></p>
</li>
<li><p><a target="_blank" href="https://docs.docker.com/engine/security/rootless/">Run Docker as a non-root user</a></p>
</li>
<li><p><a target="_blank" href="https://docs.docker.com/storage/volumes/">Docker volumes documentation</a></p>
</li>
<li><p><a target="_blank" href="https://docs.docker.com/compose/environment-variables/env-file/">Docker <code>.env</code> file documentation</a></p>
</li>
<li><p><a target="_blank" href="https://docker-minecraft-server.readthedocs.io/en/latest/">Minecraft server Docker image documentation</a></p>
</li>
<li><p><a target="_blank" href="https://fabricmc.net/use/server/">Fabric server <code>.jar</code> download (Linux/MacOS)</a></p>
</li>
<li><p><a target="_blank" href="https://fabricmc.net/use/installer/">Fabric server installer for Windows</a></p>
</li>
<li><p><a target="_blank" href="https://fabricmc.net/wiki/tutorial:side">Fabric client/server patterns</a></p>
</li>
<li><p><a target="_blank" href="https://fabricmc.net/wiki/player:tutorials:java:windows">Install Java on Windows</a></p>
</li>
<li><p><a target="_blank" href="https://blog.saleshorse.org/installing-java-on-ubuntu-without-breaking-your-shell">Installing Java on Ubuntu</a></p>
</li>
<li><p><a target="_blank" href="https://blog.saleshorse.org/an-intro-to-minecraft-mods">An Intro to Minecraft Mods</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[An Intro to Minecraft Mods]]></title><description><![CDATA[I've never played Minecraft for more than a few minutes, but my son loves it. He's expressed an interest in coding and recently said he wants to code a Minecraft mod.
I started looking into what was involved in coding a mod for Minecraft, and found i...]]></description><link>https://blog.saleshorse.org/an-intro-to-minecraft-mods</link><guid isPermaLink="true">https://blog.saleshorse.org/an-intro-to-minecraft-mods</guid><category><![CDATA[minecraft]]></category><category><![CDATA[minecraft mods]]></category><category><![CDATA[fabric]]></category><category><![CDATA[minecraft server]]></category><dc:creator><![CDATA[Sharif Elkassed]]></dc:creator><pubDate>Sun, 11 Jun 2023 05:00:25 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/EgL0EtzL0Wc/upload/9f8d1358ba7662e6b38dd534fd0e7908.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I've never played Minecraft for more than a few minutes, but my son loves it. He's expressed an interest in coding and recently said he wants to code a Minecraft mod.</p>
<p>I started looking into what was involved in coding a mod for Minecraft, and found it to be more involved than I was expecting.</p>
<p>I figured there had to be others in the same boat, so I decided to document what I learned along the way.</p>
<p>Hope this helps!</p>
<h2 id="heading-what-are-we-covering">What are we covering?</h2>
<ul>
<li><p>installing Java</p>
</li>
<li><p>enabling mods in Minecraft</p>
</li>
<li><p>Minecraft servers</p>
</li>
<li><p>setting up VS Code for Java development</p>
</li>
<li><p>making a basic mod with Fabric</p>
</li>
<li><p>testing our mod in single-player mode</p>
</li>
<li><p>installing a mod on a server</p>
</li>
<li><p>troubleshooting</p>
</li>
<li><p>theory</p>
</li>
</ul>
<h2 id="heading-what-do-i-need">What do I need?</h2>
<p>Once you start poking around online, it's easy to get overwhelmed with all of the resources on the topic of Minecraft mods.</p>
<p>There are multiple versions of Minecraft, multiple releases of those versions, multiple programming languages that can be used for mod development, and multiple modding frameworks within those language options.</p>
<p>There's also a lot of outdated information, as well as information that only relates to older versions of Minecraft. I want to be able to make mods for the newest version of Minecraft, and so that's what I outline in this blog post.</p>
<h3 id="heading-minecraft-java-edition">Minecraft Java Edition</h3>
<p>The Java Edition of Minecraft is synonymous with the "computer version" of Minecraft. If you have Minecraft installed on Windows, MacOS, or some desktop variation of Linux, then you have the Java Edition.</p>
<p>In other words, you need a computer that has Minecraft installed. This is the computer that you'll be using to develop the mod.</p>
<h3 id="heading-a-minecraft-server">A Minecraft server</h3>
<p>We get into the role of a Minecraft server later in this blog post, and in even more detail in <a target="_blank" href="https://blog.saleshorse.org/how-to-run-a-minecraft-server-multiple-approaches">Running a Minecraft Server</a>. For now, just think of a Minecraft server as a place for mods to live.</p>
<p>If you're not planning on playing multiplayer, then you don't need to worry about this step much, since the only server you'll need is a development server (which comes bundled with the Fabric framework).</p>
<p>If you want to learn more about servers, check out <a target="_blank" href="https://blog.saleshorse.org/how-to-run-a-minecraft-server-multiple-approaches">Running a Minecraft Server</a>.</p>
<h3 id="heading-a-programming-language">A programming language</h3>
<p>At the top of this section, I mentioned that Minecraft can be modified with multiple programming languages. Depending on where you're looking, you might come across resources for:</p>
<ul>
<li><p>Java</p>
</li>
<li><p>Python</p>
</li>
<li><p>JavaScript</p>
</li>
</ul>
<p>I initially wanted to use JavaScript because that's what I'm most familiar with. But, after some experimentation and research, I decided JavaScript was not worth it for this use case.</p>
<p>Why? Because it adds a layer of complexity. Even if you're coding the mod in JavaScript (or TypeScript), it's getting converted to Java at the end of the day. So while it might be a more ergonomic experience to use JavaScript, the setup and dependency overhead feels less than ideal to me.</p>
<p>To keep things as simple and direct as possible, we're going to code the mod with Java.</p>
<h3 id="heading-a-way-to-modify-minecraft">A way to modify Minecraft</h3>
<p>Now we start to get a bit more technical. We know we need a server to house the mods, but how do we <em>make</em> the mods?</p>
<p>For that, we need something that hooks into the existing Minecraft code and allows us to make changes. Modifying the Minecraft code directly doesn't seem like a great idea. After all, we could (and probably would) break something.</p>
<p>Thankfully, some smart people worked through this problem and came up with the idea of a modification framework. Frameworks provide a safer and easier way to make modifications when compared to modifying source code directly.</p>
<p>We get into the details later in this blog post. For now, the important thing to know is that frameworks package your mod into a <code>.jar</code> file, which lives on your Minecraft server (or within your local installation of Minecraft).</p>
<p><code>.jar</code> files also provide modularity -- each mod exists in its own <code>.jar</code> file. If you decide you don't want a certain mod anymore, its <code>.jar</code> file can simply be deleted from the server.</p>
<p>For Java, there are two frameworks to choose from:</p>
<ul>
<li><p>Forge</p>
</li>
<li><p>Fabric</p>
</li>
</ul>
<p>I will briefly compare the two, then explain why I chose Fabric.</p>
<h4 id="heading-forge">Forge</h4>
<p>Forge is the original framework, and the more powerful of the two. As such, it can handle more drastic modifications compared to Fabric. Additionally, Forge is better at handling "interop", meaning it's easier for mods to "talk to" other mods.</p>
<p>This all sounds great, but it comes with some tradeoffs:</p>
<p>Because it is more powerful and flexible, that also makes it more demanding of your machine's resources. If your machine isn't built for performance, it may not be able to handle Forge.</p>
<p>Forge's power and flexibility come at the cost of a larger, more complex codebase. More complexity and more code mean longer release cycles -- much longer. In fact, many mods created with Forge are still out-of-date because updating them to comply with new Minecraft versions simply takes too long.</p>
<h4 id="heading-fabric">Fabric</h4>
<p>Fabric was created out of the need for a more lightweight, performant, and maintainable framework. It can be used on a standard laptop, and mods built with Fabric can be kept up-to-date as a result of Fabric's lighter footprint.</p>
<p>Of course, tradeoffs work both ways. Because it is more lightweight, the modification capabilities are not as robust as those of Forge, and so the "interop" of Fabric mods suffers; this means it's harder for Fabric mods to talk to each other when compared to Forge.</p>
<p>I decided to use Fabric because:</p>
<ul>
<li><p>I will sometimes be working "on the go", so I need to be able to develop the mod with my standard laptop</p>
</li>
<li><p>I don't need to make complex mods that work together</p>
</li>
<li><p>I want to work with the latest version of Minecraft</p>
</li>
</ul>
<p>If you decide Forge is better for what you need, I wish you the best of luck. The rest of this guide will focus on Fabric.</p>
<h2 id="heading-setup">Setup</h2>
<p>You probably already have Minecraft installed on your computer, so the next step is to give Minecraft the ability to "run" mods.</p>
<p>It seems that there is a lot of outdated content on this topic, and more recent releases of Minecraft (<code>1.19.4</code> at the time of this writing) have different setup and configuration instructions when compared to the older releases.</p>
<p>Thankfully, Fabric has a <a target="_blank" href="https://fabricmc.net/wiki/install">wiki</a>.</p>
<h3 id="heading-fabric-launcher-client">Fabric Launcher (Client)</h3>
<p>Per the wiki, we first need to install the Fabric launcher. Before that, though -- we should address what exactly the Fabric Launcher <em>is</em>. You can think of the Fabric Launcher as a special installation of Minecraft that allows mods to be loaded into the game. If you've ever installed a different version of Minecraft under the 'Installations' tab, it's very similar to that.</p>
<p>As a prerequisite, we need to make sure Java 17 is installed on the computer that Minecraft Java Edition is installed. You most likely have Minecraft installed on Windows or MacOS, but if you have it installed on Linux, you might need some additional instructions, as the Linux instructions on the wiki don't cover certain edge cases.</p>
<p>It's also worth noting that the commands in Linux <a target="_blank" href="https://fabricmc.net/wiki/player:tutorials:install_server">CLI instructions</a> are for older versions of Minecraft. If you copy the command from these instructions, it will install Java 8 (we need Java 16+). For more detailed instructions on installing Java on Linux via the CLI, see <a target="_blank" href="https://blog.saleshorse.org/installing-java-on-ubuntu-without-breaking-your-shell">Installing Java on Ubuntu</a>.</p>
<p>I'm on Windows, so I follow the appropriate instructions to <a target="_blank" href="https://fabricmc.net/wiki/player:tutorials:java:windows">verify my Java Installation</a>.</p>
<p>Following the instructions, I check my Java installation in the Windows Command Prompt:</p>
<pre><code class="lang-bash">java -version <span class="hljs-comment"># press enter</span>

<span class="hljs-comment"># output</span>
java version <span class="hljs-string">"19.0.2"</span> 2023-01-17
Java(TM) SE Runtime Environment (build 19.0.2+7-44)
Java HotSpot(TM) 64-Bit Server VM (build 19.0.2+7-44, mixed mode, sharing)
</code></pre>
<p>I see a couple of issues:</p>
<ul>
<li><p>The Java version is 19 and not 17</p>
</li>
<li><p>I have the JRE (Java Runtime Environment), but not the JDK (Java Development Kit)</p>
</li>
</ul>
<p>I follow the link on the page to the <a target="_blank" href="https://adoptium.net/temurin/releases/?version=17">Java 17 installer</a> (Windows).</p>
<p>Per the instructions, I also select the options to set <code>JAVA_HOME</code> and the Oracle registry keys:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684802756452/c51e106c-a98a-44ad-9993-3a216f409439.png" alt class="image--center mx-auto" /></p>
<p>Once the installation completes, I <strong>close the terminal</strong>, open a new one, and proceed to check the Java installation once more:</p>
<pre><code class="lang-powershell">java <span class="hljs-literal">-version</span> <span class="hljs-comment"># press enter</span>

<span class="hljs-comment"># output</span>
openjdk version <span class="hljs-string">"17.0.7"</span> <span class="hljs-number">2023</span><span class="hljs-literal">-04</span><span class="hljs-literal">-18</span>
OpenJDK Runtime Environment Temurin<span class="hljs-literal">-17</span>.<span class="hljs-number">0.7</span>+<span class="hljs-number">7</span> (build <span class="hljs-number">17.0</span>.<span class="hljs-number">7</span>+<span class="hljs-number">7</span>)
OpenJDK <span class="hljs-number">64</span><span class="hljs-literal">-Bit</span> Server VM Temurin<span class="hljs-literal">-17</span>.<span class="hljs-number">0.7</span>+<span class="hljs-number">7</span> (build <span class="hljs-number">17.0</span>.<span class="hljs-number">7</span>+<span class="hljs-number">7</span>, mixed mode, sharing)
</code></pre>
<p>Everything reads as version 17 (including the JRE), so I move on to the next step of installing the launcher, again following the <a target="_blank" href="https://fabricmc.net/wiki/player:tutorials:install_mcl:windows">instructions</a>:</p>
<ul>
<li><p>download the Fabric Installer <code>.exe</code> file</p>
</li>
<li><p>run the <code>.exe</code> file and choose the latest version (<code>1.19.4</code>)</p>
<ul>
<li>keep all other defaults</li>
</ul>
</li>
<li><p>create a new folder for the launcher installation: <code>C:\myUser\fabric-minecraft</code></p>
</li>
<li><p>open Minecraft Launcher</p>
</li>
<li><p>go to the 'Installations' tab</p>
</li>
<li><p>find the launcher installation</p>
<ul>
<li><p>press the '...' button</p>
</li>
<li><p>select 'Edit'</p>
</li>
<li><p>change the 'Game Directory' from the default location to the folder from the step above</p>
</li>
<li><p>press 'Save'</p>
</li>
</ul>
</li>
<li><p>back on the main screen, press 'Play' so that Minecraft installs in that folder</p>
<ul>
<li>this step will also add the <code>/mods</code> folder to the folder you selected for installation</li>
</ul>
</li>
<li><p>close Minecraft</p>
</li>
<li><p>download the <a target="_blank" href="https://modrinth.com/mod/fabric-api/version/0.81.1+1.19.4">Fabric API</a></p>
</li>
<li><p>place the Fabric API <code>.jar</code> file in <code>C:\myUser\fabric-minecraft\mods</code></p>
</li>
<li><p>download the <a target="_blank" href="https://modrinth.com/mod/modmenu/version/6.2.2">Mod Menu mod</a> (so we can make sure mods can load)</p>
</li>
<li><p>place it in the <code>\fabric-minecraft\mods</code> folder</p>
</li>
<li><p>open Minecraft and confirm you now have the 'Mods' menu on the home screen</p>
</li>
</ul>
<p>So, what did we just <em>do</em>?</p>
<p>We just created a custom version of Minecraft that enables modifications (mods).</p>
<p>The Fabric API itself <em>is a mod</em>. We can think of it as the "parent mod", "controller mod", etc. -- basically, we need the Fabric API mod for the other mods to run against. When you drop a mod into the <code>/mods</code> folder, it must be a mod that is supported by the Fabric API (or one you developed using the Fabric API).</p>
<h3 id="heading-fabric-server">Fabric Server</h3>
<p>At this point, you might wonder what we need a server for. We already have a version of Minecraft where we can install mods, so what role does the server play?</p>
<p>One use case for a server is to play multiplayer with mods. Think about it -- let's say we download and install a few more mods, and we want to use those mods to play with friends online.</p>
<p>Right now, we don't have a way of doing that without joining an existing server that someone else created. When you play Single Player mode, it uses the Minecraft code (along with any mods) that lives on your computer's hard drive.</p>
<p>To play Multiplayer with mods, we need to load the Minecraft code from a hard drive that our friends can access, too -- a hard drive that is accessible either via a local or public network.</p>
<p>And for that, we need a server.</p>
<p>A Fabric server takes the same concept as the Fabric Launcher and puts it into the context of a server. This way, your friends can connect to that server and enjoy the mods that you've installed in the server's <code>/mods</code> folder.</p>
<p>But... wait a second, we're interested in making mods, not playing multiplayer -- why are we talking about this?</p>
<p>I just wanted to drive home the concept of what a Minecraft server is and what it does. Now that we understand that, we can move on to a more relevant use case for a server -- a development server!</p>
<p>When we write code to modify Minecraft, we need to test to make sure the code works the way we expect when the game is running; that's what a development server allows us to do. We're connecting a server whose sole purpose is for testing our mod code. Since it's just for testing and development, the development server is run from your local machine.</p>
<p>There's no need for the development server once you're done developing your mod.</p>
<p>Let's pretend that we want to code a mod that we eventually play with friends; here's what that might look like:</p>
<ul>
<li><p>install an instance of Minecraft locally via the Fabric Launcher (we did this in the last step)</p>
</li>
<li><p>set up our development environment for Java in VS Code</p>
</li>
<li><p>spin up a local development server that has the Fabric API running on it</p>
</li>
<li><p>set up a network-accessible server that has the Fabric API running on it</p>
</li>
<li><p>develop our mod against the local Fabric development server</p>
</li>
<li><p>once we're finished with the mod, it will exist in a <code>.jar</code> file</p>
</li>
<li><p>we can test the finished mod in single-player by putting that <code>.jar</code> file into our <code>/mods</code> folder within our Fabric Launcher installation</p>
</li>
<li><p>we decide the mod is ready for prime time, and copy the <code>.jar</code> file into the <code>/mods</code> folder on our network-accessible Fabric server</p>
</li>
<li><p>our friends connect to the server and play our mod</p>
</li>
</ul>
<p>We've already installed the local Minecraft instance with the Fabric Launcher. The next step is to set up our development environment in VS Code.</p>
<p>Let's get into it!</p>
<h3 id="heading-development-environment">Development Environment</h3>
<p>This guide will cover how to set up your development environment for VS Code. If you're using something else, you can find instructions on the <a target="_blank" href="https://fabricmc.net/wiki/tutorial:setup">Fabric wiki</a> .</p>
<p>Your development environment needs to be on the computer where Minecraft is installed. This is because running the Java project in VS Code (or whatever you're using) opens the Minecraft client. The Minecraft client then interacts with the development server so that you can test the current state of your mod.</p>
<h4 id="heading-a-note-on-wsl">A note on WSL</h4>
<p>If you're not a Windows user (or don't use WSL), you can skip ahead to the next section. If you're a Windows user that normally develops in WSL (like me), you'll need to use plain old Windows for your development environment -- <strong><mark>not</mark></strong> <mark>WSL</mark>.</p>
<p>I came to this conclusion after many hours of troubleshooting. I never found an official answer to validate my assumptions, but what I gather is this:</p>
<p>When you run your mod's Java project, VS Code opens up Minecraft and temporarily loads your mod into the game so you can test it. For VS Code to be able to do that, it needs to be able to "see" where Minecraft is installed on your computer.</p>
<p>Even though the Windows file system can be accessed via WSL, Microsoft doesn't recommend cross-communication between the two systems. They're meant to be used independently. Because of this, you'll get errors if you try and run the Java project from WSL (since Minecraft is installed on the Windows filesystem).</p>
<p>For this same reason, if you're running your (non-development) Fabric server locally, you'll want to run the server from within Windows and not WSL. This is because Windows can't "see" that there is a server running on WSL (unless you're running the server in a Docker container). The <a target="_blank" href="https://blog.saleshorse.org/how-to-run-a-minecraft-server-multiple-approaches">blog post on running a Minecraft server</a> should provide some more context.</p>
<h3 id="heading-setting-up-vs-code">Setting up VS Code</h3>
<p>In this section, we download an example mod template and make the necessary setup and configuration changes that allow us to run the mod from VS Code.</p>
<p>We start by following the 'Manual Steps' on the <a target="_blank" href="https://fabricmc.net/wiki/tutorial:setup">wiki</a></p>
<p>(I'm doing this from a Powershell prompt on Windows):</p>
<ul>
<li><p>go to the <a target="_blank" href="https://github.com/FabricMC/fabric-example-mod/">example mod git repo</a></p>
</li>
<li><p>clone the repo</p>
<ul>
<li><code>git clone</code><a target="_blank" href="https://github.com/FabricMC/fabric-example-mod.git"><code>https://github.com/FabricMC/fabric-example-mod.git</code></a></li>
</ul>
</li>
<li><p>change into the repo directory we just cloned</p>
<ul>
<li><code>cd fabric-example-mod</code></li>
</ul>
</li>
<li><p>open the repo in VS Code</p>
<ul>
<li><code>code .</code> (there is a space between the word <code>code</code> and the period)</li>
</ul>
</li>
<li><p>delete the <code>LICENSE</code> and <code>README.md</code> files</p>
</li>
</ul>
<p>Before we start developing, we need to make sure VS Code is set up for Java by installing the necessary extensions.</p>
<ul>
<li><p><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=vscjava.vscode-java-pack">Extension Pack for Java</a></p>
</li>
<li><p><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=vscjava.vscode-gradle">Gradle for Java</a></p>
</li>
</ul>
<p>Go ahead and install the extensions. You may need to reload VS Code once they are finished installing for the changes to take effect.</p>
<p>You might see an error like the one below, but we'll fix that shortly:</p>
<pre><code class="lang-java">The supplied phased action failed with an exception.
A problem occurred configuring root project <span class="hljs-string">'fabric-example-mod'</span>.
Failed to setup Minecraft, java.nio.file.ClosedFileSystemException: <span class="hljs-keyword">null</span>
</code></pre>
<p>Before we address the error, let's modify the mappings in our <code>gradle.properties</code> file. Fabric helps us out by providing the values on <a target="_blank" href="https://fabricmc.net/develop/">this page</a>.</p>
<p>The instructions say we also need to edit the <code>maven_group</code> and <code>archives_base_name</code> properties in our <code>gradle.properties</code> file. These settings are related to how a mod is packaged, versioned, and distributed. Since we're not publishing a mod, and we're only concerned with the fundamentals of <em>how to make a mod</em> in this guide, we're not going to concern ourselves with these settings for now.</p>
<p>In other words, just put <em>something</em> there. Here's what my file looks like:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># gradle.properties</span>

<span class="hljs-comment"># Done to increase the memory available to gradle.</span>
org.gradle.jvmargs=-Xmx1G
org.gradle.parallel=<span class="hljs-literal">true</span>

<span class="hljs-comment"># Fabric Properties</span>
<span class="hljs-comment"># check these on https://fabricmc.net/develop</span>
minecraft_version=1.19.4
yarn_mappings=1.19.4+build.2
loader_version=0.14.19

<span class="hljs-comment"># Mod Properties</span>
mod_version = 1.0.0
maven_group = sharif.sharif.test.mod
archives_base_name = sharif-test-mod

<span class="hljs-comment"># Dependencies</span>
fabric_version=0.82.0+1.19.4
</code></pre>
<p>After saving the file, this message appears in the corner of VS Code. Press 'Yes':</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1685015512191/3e7b015a-2493-4885-bfff-920abd9114eb.png" alt class="image--center mx-auto" /></p>
<p>We then see this message in the 'Problems' tab of the VS Code terminal area:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1685015594287/a9a1a220-d333-421b-8094-2d3222c1525f.png" alt class="image--center mx-auto" /></p>
<p>Now we can address the error.</p>
<p>If we look at <a target="_blank" href="https://fabricmc.net/wiki/tutorial:vscode_setup">Fabric's VS Code Setup documentation</a>, it tells us that we need to run the <code>vscode</code> task.</p>
<pre><code class="lang-bash">./gradlew vscode
</code></pre>
<p>Once this command runs, the error should go away (close and re-open VS Code if it doesn't). The error was because we hadn't run the Gradle configuration that was specific to VS Code.</p>
<p>Now we should be able to run the following command without any issues:</p>
<pre><code class="lang-bash">./gradlew --stop <span class="hljs-comment"># press enter</span>
./gradlew <span class="hljs-comment"># press enter</span>

<span class="hljs-comment"># the script runs for a bit, then gives the following message when finished</span>
BUILD SUCCESSFUL <span class="hljs-keyword">in</span> 12s
1 actionable task: 1 executed
</code></pre>
<p>If <code>./gradlew</code> fails and your'e on Windows, you may just need to run <code>./gradlew.bat</code>. If it fails and you're not on Windows, it's probably a problem with your <code>JAVA_HOME</code> environment variable.</p>
<p>Now we're ready to generate the Minecraft source code</p>
<pre><code class="lang-bash">./gradlew -- stop <span class="hljs-comment"># just in case</span>
./gradlew genSources <span class="hljs-comment"># this will run for awhile</span>
</code></pre>
<p>When we ran the <code>genSources</code> command, it downloaded the Minecraft source code onto our computer. We'll dig into that a bit more in a later section.</p>
<p>For now, we're finally able to run the project to see if it works. To do that, either press <code>Ctrl + F5</code>, or go to <code>Run -&gt; Run Without Debugging</code> in VS Code.</p>
<p>You'll see some output in the terminal, and then Minecraft should open up.</p>
<p>Alright, so we've run the build, but how do we actually <em>make a mod?!</em></p>
<p>Patience, young Glow Squid; we do that next.</p>
<h2 id="heading-making-modifications">Making Modifications</h2>
<p>The moment we've all been waiting for!</p>
<p>Below, we add a new item to the game by following the tutorial provided by the Fabric wiki.</p>
<h3 id="heading-adding-an-item">Adding an Item</h3>
<p>This section is merely a more detailed version of the <a target="_blank" href="https://fabricmc.net/wiki/tutorial:items">instructions for adding an item to the game</a>.</p>
<ul>
<li><p>open our <code>ExampleMod.java</code> file</p>
<ul>
<li><p>can press <code>Ctrl + P</code> and search for 'Example'</p>
</li>
<li><p>or navigate directly: <code>/src/main/java/net/fabricmc/example/ExampleMod.java</code></p>
</li>
</ul>
</li>
</ul>
<p>Once we make the suggested changes to the file, it looks like this:</p>
<pre><code class="lang-java"><span class="hljs-keyword">package</span> net.fabricmc.example;

<span class="hljs-keyword">import</span> org.slf4j.Logger;
<span class="hljs-keyword">import</span> org.slf4j.LoggerFactory;

<span class="hljs-keyword">import</span> net.fabricmc.api.ModInitializer;
<span class="hljs-keyword">import</span> net.fabricmc.fabric.api.item.v1.FabricItemSettings;
<span class="hljs-keyword">import</span> net.minecraft.item.Item;
<span class="hljs-keyword">import</span> net.minecraft.registry.Registries;
<span class="hljs-keyword">import</span> net.minecraft.registry.Registry;
<span class="hljs-keyword">import</span> net.minecraft.util.Identifier;

<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ExampleMod</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">ModInitializer</span> </span>{

    <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> Logger LOGGER = LoggerFactory.getLogger(<span class="hljs-string">"modid"</span>);

    <span class="hljs-comment">// an instance of our new item</span>
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> Item CUSTOM_ITEM = <span class="hljs-keyword">new</span> Item(<span class="hljs-keyword">new</span> FabricItemSettings());

    <span class="hljs-meta">@Override</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">onInitialize</span><span class="hljs-params">()</span> </span>{

        LOGGER.info(<span class="hljs-string">"Hello Fabric world!"</span>);
        Registry.register(Registries.ITEM, <span class="hljs-keyword">new</span> Identifier(<span class="hljs-string">"tutorial"</span>, <span class="hljs-string">"custom_item"</span>), CUSTOM_ITEM);

    }
}
</code></pre>
<p>Now we can go to the 'Run' menu in VS Code and select 'Run without Debugging' again.</p>
<p>Once the game loads, we enter singleplayer mode and type the following command (type the <code>/</code> key to bring up the in-game chat):</p>
<p><code>/give @s tutorial:custom_item</code></p>
<p>You should see the item appear in your player's hand and inventory!</p>
<p>If you want to do more things with the new item, like add a new texture to the item, trigger sounds with the item, etc., go ahead and continue with the <a target="_blank" href="https://fabricmc.net/wiki/tutorial:items">instructions on the Fabric wiki</a>. For the scope of this blog post, we're only interested in the fact that we were able to successfully make a mod.</p>
<h3 id="heading-getting-your-mod-into-a-jar-file">Getting your mod into a <code>.jar</code> file</h3>
<p>Now that we've made a mod, we want to be able to use it in an actual instance of Minecraft, not just the sandboxed world that we get when we run the mod in VS Code.</p>
<p>All mods (including Fabric itself) are packaged in a <code>.jar</code> file. That means we need to get our mod into a <code>.jar</code> file.</p>
<p>To do that, we need to run the 'build' task:</p>
<ul>
<li><p>in VS Code, right-click on the <code>build.gradle</code> file</p>
<ul>
<li><p>select 'Show Gradle Tasks'</p>
</li>
<li><p>run the 'build' task</p>
</li>
</ul>
</li>
<li><p>build is output to <code>build/libs</code></p>
</li>
<li><p>the file you want is the one <strong><mark>without</mark></strong> <code>-sources</code> in the filename</p>
</li>
</ul>
<p>Now that our mod is nicely packaged into that <code>.jar</code> file, we're ready to put it in the <code>/mods</code> folder.</p>
<ul>
<li><p>if using the mod in single-player mode, this refers to the <code>/mods</code> folder within the installation directory of the Fabric installation we set up in the <strong>Fabric Launcher (Client)</strong> section above</p>
<ul>
<li>if you installed the <em>Mod Menu</em> mod in that section, you can use the 'open mods folder' button that is available from the Mod Menu interface</li>
</ul>
</li>
<li><p>if using the mod on a server, this refers to the <code>/mods</code> directory on the server</p>
<ul>
<li><p>the way this directory is accessed varies depending on how the server is set up</p>
</li>
<li><p>more info in <a target="_blank" href="https://blog.saleshorse.org/how-to-run-a-minecraft-server-multiple-approaches">Running a Minecraft Server</a></p>
</li>
</ul>
</li>
</ul>
<p>Once your <code>.jar</code> file is in its appropriate <code>/mods</code> folder, we're ready to test it out:</p>
<ul>
<li><p>create a new world with cheats enabled (so that we can use commands)</p>
</li>
<li><p>run the <code>/give</code> command that we ran earlier when we were in our development environment</p>
<ul>
<li><code>/give @s tutorial:custom_item</code></li>
</ul>
</li>
</ul>
<p>If you look at the Mod Menu interface after copying your <code>.jar</code> file to the <code>/mods</code> folder, you will have noticed that the mod still has all of the default tutorial values -- the name, thumbnail and descriptions are all generic.</p>
<p>A good way to get comfortable with the structure of a Fabric project is to try and change all of these values to something of your choosing, then re-install the mod with your new changes.</p>
<h3 id="heading-other-ways-to-modify-minecraft">Other Ways to Modify Minecraft</h3>
<p>Now that we've successfully created a Minecraft mod, I think it's important to understand how that fits into the bigger picture of the modding paradigm.</p>
<p>We can think of a modding framework like Fabric as having many layers. We make changes at whatever layer is appropriate for the mod in question. More complex mods require making changes at deeper layers.</p>
<p>To better understand these layers, take a look at the <a target="_blank" href="https://fabricmc.net/wiki/tutorial:introduction">Introduction to Modding page</a> in the wiki.</p>
<p>The recommended approach is to start at the outer layer (Native Minecraft APIs), and only move to the next layer if the current layer doesn't meet your needs.</p>
<p>For example, the item we created in the last section made use of the Registry, a Native Minecraft API.</p>
<p>The wiki does a better job than I can at explaining most of the other layers, but I do want to call attention to Mixins because it draws on some fundamental Java knowledge that you may not already be equipped with.</p>
<h4 id="heading-mixins">Mixins</h4>
<p>If you looked at the <a target="_blank" href="https://fabricmc.net/wiki/tutorial:introduction">wiki</a>, you will have seen that Mixins are the innermost layer of the modding framework, and that Mixins allow the Minecraft source code to be changed.</p>
<p>I wanted to bring this up because it ties back into the <code>genSources</code> command we ran earlier when setting up our development environment in VS Code. The <code>genSources</code> command downloads the Minecraft source code onto our computer so that we can view it (see FAQ <a target="_blank" href="https://fabricmc.net/wiki/tutorial:reading_mc_code">here</a>).</p>
<p>Before Mixins were a thing, the source code needed to be modified directly. As you might imagine, this can lead to problems. Thus, Mixins were created as a layer of abstraction so that the source code could be modified more safely and predictably.</p>
<p>Since we don't need to modify the source code directly, reading the source code isn't as critical as it was before Mixins came along, but it's still good to know how everything works behind the scenes.</p>
<h4 id="heading-bytecode">Bytecode</h4>
<p>Before we get into viewing the bytecode, it's important to understand what bytecode is.</p>
<p>Bytecode is what allows Java to run on any platform, by way of the JVM (Java Virtual Machine); it's an intermediary step between a <code>.java</code> file and binary. Java bytecode is represented by a <code>.class</code> file.</p>
<p>For example, if we have a <code>.java</code> file on our Windows machine, and we want to share it with someone on MacOS, simply giving them the <code>.java</code> file won't work because it's a different operating system and therefore processes <code>.java</code> files differently.</p>
<p>This is where the JVM comes into play.</p>
<p>Windows, MacOS, and Linux all have their own OS-specific versions of the JVM. So if we go back to our scenario, but this time make use of the JVM, it looks something like this:</p>
<ul>
<li><p>we have a <code>.java</code> file on our Windows machine</p>
</li>
<li><p>the JVM converts our <code>.java</code> file to a <code>.class</code> (bytecode) file</p>
</li>
<li><p>we share the <code>.class</code> file with someone on MacOS</p>
<ul>
<li>Java is installed on the Mac</li>
</ul>
</li>
<li><p>because Java is installed, the Mac can use the Mac version of the JVM to convert the <code>.class</code> file into binary code that MacOS understands</p>
</li>
</ul>
<p>Again, you probably won't need to dig into bytecode, even if you do need to make modifications at the Mixin level, but you never know when it might come in handy.</p>
<p>By default, VS Code is not set up to view bytecode, but it can be enabled with an <a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=mnxn.jvm-bytecode-viewer">extension</a>.</p>
<h2 id="heading-closing-thoughts">Closing thoughts</h2>
<p>I'm aware that this blog post probably seemed like a whole lot of talking and setup, and not a lot of modding -- that was kind of the point. The setup and theory behind Minecraft mods were what I found to be the most challenging and elusive, and so that's what I've covered.</p>
<p>I may create more blog posts in the future that dive deeper into the details of the code, but the purpose of this blog post was to build a foundation and get you started.</p>
<p>Speaking of building a foundation, there was a lot of information that I couldn't logically fit into this blog post, so I've broken that info out into separate posts:</p>
<ul>
<li><p><a target="_blank" href="https://blog.saleshorse.org/how-to-run-a-minecraft-server-multiple-approaches">Running a Minecraft Server (multiple approaches)</a></p>
</li>
<li><p><a target="_blank" href="https://blog.saleshorse.org/installing-java-on-ubuntu-without-breaking-your-shell">Installing Java on Ubuntu (without breaking your shell)</a></p>
</li>
</ul>
<p>These links are also included in the Resources section below.</p>
<h2 id="heading-resources">Resources</h2>
<ul>
<li><p><a target="_blank" href="https://blog.saleshorse.org/how-to-run-a-minecraft-server-multiple-approaches">Running a Minecraft Server (multiple approaches)</a></p>
</li>
<li><p><a target="_blank" href="https://blog.saleshorse.org/installing-java-on-ubuntu-without-breaking-your-shell">Installing Java on Ubuntu (without breaking your shell)</a></p>
</li>
<li><p><a target="_blank" href="https://github.com/FabricMC/fabric-example-mod">Fabric example mod GitHub repo</a></p>
</li>
<li><p><a target="_blank" href="https://fabricmc.net/wiki/start">Fabric wiki</a></p>
</li>
<li><p><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=vscjava.vscode-java-pack">VS Code Java extension pack</a></p>
</li>
<li><p><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=mnxn.jvm-bytecode-viewer">JVM Bytecode Viewer extension</a></p>
</li>
<li><p><a target="_blank" href="https://www.digitalocean.com/community/tutorials/how-to-install-java-with-apt-on-ubuntu-22-04">How To Install Java with Apt on Ubuntu 22.04 (Digital Ocean blog)</a></p>
</li>
<li><p><a target="_blank" href="https://www.youtube.com/watch?v=_JZ7bnk3oiM">Suited Llamas Fabric Mod Tutorial</a></p>
</li>
<li><p><a target="_blank" href="https://www.youtube.com/watch?v=b2W1Xb7nLKo">Trouble Chute's 1.19+ Fabric Server Tutorial</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Installing Java on Ubuntu (without breaking your shell)]]></title><description><![CDATA[TL;DR -- Different shells store environment variables in different locations. Set environment variables using your shell's recommended location.

There are plenty of resources out there for installing Java on Ubuntu, but I wanted to write one that co...]]></description><link>https://blog.saleshorse.org/installing-java-on-ubuntu-without-breaking-your-shell</link><guid isPermaLink="true">https://blog.saleshorse.org/installing-java-on-ubuntu-without-breaking-your-shell</guid><category><![CDATA[Java]]></category><category><![CDATA[Ubuntu]]></category><category><![CDATA[zsh]]></category><category><![CDATA[terminal]]></category><category><![CDATA[shell]]></category><dc:creator><![CDATA[Sharif Elkassed]]></dc:creator><pubDate>Thu, 08 Jun 2023 04:48:17 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/SCzYpM9_HLA/upload/920afdca844114831d5b5bcd1ced6564.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p>TL;DR -- Different shells store environment variables in different locations. Set environment variables using your shell's recommended location.</p>
</blockquote>
<p>There are plenty of resources out there for installing Java on Ubuntu, but I wanted to write one that covers the exact issue I ran into when installing Java with <code>zsh</code>.</p>
<p>This post focuses on the process for installing Java with <code>zsh</code> <em>as compared to</em> installing Java with <code>bash</code>. If you've no interest in <code>zsh</code> and just want to know how to bang it out with <code>bash</code>, I suggest reading <a target="_blank" href="https://www.digitalocean.com/community/tutorials/how-to-install-java-with-apt-on-ubuntu-22-04">this Digital Ocean post</a> instead.</p>
<h2 id="heading-update-package-manager">Update Package Manager</h2>
<p>Before doing anything, it's a good idea to update our package manager:</p>
<pre><code class="lang-bash">sudo apt update
sudo apt upgrade
</code></pre>
<p>With that out of the way, the first thing I like to do is clear out any existing Java installations before making any other changes.</p>
<h2 id="heading-check-the-existing-java-installation">Check the existing Java installation</h2>
<p>We can start by seeing what our existing Java setup looks like by issuing a command:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># WSL</span>
java -version <span class="hljs-comment"># press enter</span>

<span class="hljs-comment"># output</span>
openjdk version <span class="hljs-string">"17.0.5"</span> 2022-10-18
OpenJDK Runtime Environment (build 17.0.5+8-Ubuntu-2ubuntu122.04)
OpenJDK 64-Bit Server VM (build 17.0.5+8-Ubuntu-2ubuntu122.04, mixed mode, sharing)
</code></pre>
<h2 id="heading-remove-existing-java-installation">Remove existing Java installation</h2>
<p>You may or may not want to remove an existing Java installation. I prefer to start fresh, just in case.</p>
<p>In the previous step, I saw that I had the OpenJDK packages installed for Java, so I proceed with removing them.</p>
<blockquote>
<p>Note: the <code>comments</code> add context (actions such as tab completions, saving files, re-opening the terminal)</p>
</blockquote>
<pre><code class="lang-bash"><span class="hljs-comment"># WSL</span>
sudo apt remove --autoremove openjdk- <span class="hljs-comment"># press tab repeteadly</span>

<span class="hljs-comment"># installed packages should be listed with tab completion</span>

<span class="hljs-comment"># after pressing tab, it autocompletes to:</span>
sudo apt remove --autoremove openjdk-17-jre-headless:amd64 <span class="hljs-comment"># press enter</span>

<span class="hljs-comment"># if more than one package appeared in the previous step, make sure to remove all of them</span>

<span class="hljs-comment"># source the shell (or close and re-open your terminal application)</span>
<span class="hljs-built_in">source</span> ~/.zshrc

<span class="hljs-comment"># confirm Java is no longer installed</span>
java -version <span class="hljs-comment"># press enter</span>

<span class="hljs-comment"># output</span>
zsh: <span class="hljs-built_in">command</span> not found: java
</code></pre>
<h2 id="heading-install-desired-java-version">Install desired Java version</h2>
<p>Now we can install the appropriate version of Java (more specifically, the JDK). In my case, it's version <code>17</code>.</p>
<p>So, we can just add <code>17</code> to <code>openjdk-</code> in the command below (replace <code>17</code> with whatever version you need to install):</p>
<pre><code class="lang-bash">sudo apt install openjdk-17 <span class="hljs-comment"># press tab to see packages listed</span>

<span class="hljs-comment"># output</span>
openjdk-17-dbg           openjdk-17-jdk           openjdk-17-jre-headless
openjdk-17-demo          openjdk-17-jdk-headless  openjdk-17-jre-zero
openjdk-17-doc           openjdk-17-jre           openjdk-17-source

<span class="hljs-comment"># we want a headless JDK, we so end up with:</span>
sudo apt install openjdk-17-jdk-headless

<span class="hljs-comment"># source the shell or restart the terminal</span>
<span class="hljs-built_in">source</span> ~/.zshrc

<span class="hljs-comment"># confirm Java installation</span>
java -version <span class="hljs-comment"># press enter</span>

<span class="hljs-comment"># output</span>
openjdk version <span class="hljs-string">"17.0.7"</span> 2023-04-18
OpenJDK Runtime Environment (build 17.0.7+7-Ubuntu-0ubuntu122.04.2)
OpenJDK 64-Bit Server VM (build 17.0.7+7-Ubuntu-0ubuntu122.04.2, mixed mode, sharing)
</code></pre>
<h2 id="heading-set-the-dollarjavahome-environment-variable">Set the <code>$JAVA_HOME</code> environment variable</h2>
<p>We're not quite done yet, though. We need to set the <code>$JAVA_HOME</code> environment variable.</p>
<p>I'll first describe the way I initially tried it so that I can illustrate how much more straightforward the correct way is.</p>
<h3 id="heading-the-wrong-way">The wrong way</h3>
<p>This is where I got thrown off by <code>zsh</code>. I didn't realize that <code>zsh</code> handles environment variables differently than <code>bash</code>.</p>
<p>I made the mistake of adding <code>JAVA_HOME</code> to my <code>/etc/environment</code> file (the <code>bash</code> way). The result went something like this:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># /etc/environment</span>
JAVA_HOME=<span class="hljs-string">"/path/to/java/home"</span>

<span class="hljs-comment"># save and exit /etc/environment</span>

<span class="hljs-comment"># source the file</span>
<span class="hljs-built_in">source</span> /etc/environment

<span class="hljs-built_in">echo</span> <span class="hljs-variable">$JAVA_HOME</span> <span class="hljs-comment"># outputs /path/to/java/home, yay!</span>

<span class="hljs-comment"># close terminal </span>
<span class="hljs-comment"># open new terminal</span>
<span class="hljs-built_in">echo</span> <span class="hljs-variable">$JAVA_HOME</span> <span class="hljs-comment"># no output, uhh.. wtf?</span>
</code></pre>
<p>At this point, I thought I would get clever and source <code>/etc/environment</code> within my <code>.zshrc</code></p>
<p>It <em>almost</em> worked:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># .zshrc</span>
<span class="hljs-built_in">source</span> /etc/environment

<span class="hljs-comment"># save and exit .zshrc</span>
<span class="hljs-comment"># close and re-open terminal</span>

<span class="hljs-built_in">echo</span> <span class="hljs-variable">$JAVA_HOME</span> <span class="hljs-comment"># success!</span>
</code></pre>
<p>So far so good, but when I try to open a project with VS Code from the terminal...</p>
<pre><code class="lang-bash">code . <span class="hljs-comment"># presss enter</span>

zsh: <span class="hljs-built_in">command</span> not found: code
</code></pre>
<p>GRRR!</p>
<p>I decide to get clever once again, and add my entire <code>$PATH</code> variable to my <code>/etc/environment</code> file:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># /etc/environment</span>
PATH=<span class="hljs-string">"/home/this/path:/home/that/path:/another/path:/vs/code/path"</span>
JAVA_HOME=<span class="hljs-string">"/java/home/path"</span>
</code></pre>
<p>This solution actually worked, but it felt so hacky. I just knew there had to be a better way...</p>
<h3 id="heading-the-right-way">The right way</h3>
<p>Before we jump in, note that the configuration has been reset from the previous section, (titled<em>The wrong way):</em></p>
<ul>
<li><p><code>source /etc/environment</code> removed from <code>.zshrc</code></p>
</li>
<li><p><code>JAVA_HOME</code> variable deleted from <code>/etc/environment</code></p>
</li>
<li><p><code>PATH</code> deleted from <code>/etc/environment</code></p>
</li>
<li><p>the terminal was session closed and re-opened</p>
</li>
</ul>
<p>Now we can go about setting <code>JAVA_HOME</code> properly:</p>
<pre><code class="lang-bash"><span class="hljs-built_in">echo</span> <span class="hljs-variable">$JAVA_HOME</span> <span class="hljs-comment"># press enter</span>
<span class="hljs-comment"># no output </span>

<span class="hljs-comment"># find the Java install path</span>
sudo update-alternatives --config java <span class="hljs-comment"># press enter</span>

<span class="hljs-comment"># output</span>
There is only one alternative <span class="hljs-keyword">in</span> link group java (providing /usr/bin/java): 
/usr/lib/jvm/java-17-openjdk-amd64/bin/java
Nothing to configure.
</code></pre>
<p>From here we can copy the path that was output: <code>/usr/lib/jvm/java-17-openjdk-amd64/bin/java</code></p>
<p><a id="shell-environment-table"></a></p>
<p>The place you put that path will depend on a few factors:</p>
<ul>
<li><p>which shell you're using</p>
</li>
<li><p>whether this Java configuration is being set at the profile level or the system level</p>
</li>
</ul>
<div class="hn-table">
<table>
<thead>
<tr>
<td></td><td><code>bash</code></td><td><code>zsh</code></td></tr>
</thead>
<tbody>
<tr>
<td>profile-level</td><td><code>~/.bashrc</code></td><td><code>~/.zshenv</code></td></tr>
<tr>
<td>system-level</td><td><code>/etc/environment</code></td><td><code>/etc/zsh/zshenv</code></td></tr>
</tbody>
</table>
</div><p>Luckily, what we put in the file is the same, regardless of scope or shell:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># at the bottom of the appropriate file from the table above</span>
JAVA_HOME=<span class="hljs-string">"/usr/lib/jvm/java-17-openjdk-amd64/bin/java"</span>
</code></pre>
<p>Save and exit the file, then <strong>close and re-open the terminal</strong>.</p>
<p>Now we can see that <code>JAVA_HOME</code> is set if we echo it out:</p>
<pre><code class="lang-bash"><span class="hljs-built_in">echo</span> <span class="hljs-variable">$JAVA_HOME</span>

<span class="hljs-comment"># output</span>
/usr/lib/jvm/java-17-openjdk-amd64/bin/java
</code></pre>
<p>That's it! Your Java configuration is ready to go, and your shell commands remain intact.</p>
]]></content:encoded></item><item><title><![CDATA[Learning chroot]]></title><description><![CDATA[I'm currently working on a side project that will allow users from the public Internet to access a Linux server that I manage.
As you might imagine, there are a host of security concerns that come along with this requirement. I started looking for so...]]></description><link>https://blog.saleshorse.org/learning-chroot</link><guid isPermaLink="true">https://blog.saleshorse.org/learning-chroot</guid><category><![CDATA[Linux]]></category><category><![CDATA[linux for beginners]]></category><category><![CDATA[containers]]></category><category><![CDATA[Security]]></category><dc:creator><![CDATA[Sharif Elkassed]]></dc:creator><pubDate>Thu, 18 May 2023 14:01:09 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/TNHmdU8fN50/upload/e3a3e9d46bd506fd055a68ddb80f84db.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I'm currently working on a side project that will allow users from the public Internet to access a Linux server that I manage.</p>
<p>As you might imagine, there are a host of security concerns that come along with this requirement. I started looking for solutions and came across the idea of a chroot.</p>
<blockquote>
<p><strong>NOTE:</strong> I will be learning as I write this. This post is not intended to be a tutorial, but more a log of my working sessions and debugging efforts with this topic -- recording my thought process and what I learned along the way in a very informal matter.</p>
</blockquote>
<h2 id="heading-what-is-chroot">What is chroot?</h2>
<p>The idea is to create an isolated environment for a user (or users) so that they are limited in what they can see and do.</p>
<p>This is similar to the idea of a Docker container, but not quite the same.</p>
<p>A chroot lives inside an existing Linux install. You can almost think of it as an operating system within an operating system. Even if there are technically other users outside the chroot, the users inside the chroot are not aware of their existence. This is why a chroot is sometimes referred to as a "chroot jail".</p>
<p>The <code>root</code> user within a chroot is a sort of pseudo root. They <em>do</em> have root privileges within their sectioned-off operating system (the chroot jail), but those root privileges are being provisioned by the <em>true</em> root user, the one who set up the chroot.</p>
<h2 id="heading-chroot-creation-first-pass">Chroot creation -- first pass</h2>
<p>I'm currently using a Raspberry Pi as my development server for this project, so that is where I'll be implementing the chroot.</p>
<p>I start by following <a target="_blank" href="https://help.ubuntu.com/community/BasicChroot">this guide</a>. The instructions are for Ubuntu (which is not what the Raspberry Pi is running), but the instructions here are more clear to me than others I've found so I'm going to try and make it work.</p>
<p>I install the <code>schroot</code> and <code>debootstrap</code> packages as instructed, create my <code>/var/chroot</code> directory, then open the <code>/etc/schroot/schroot.conf</code> file. I know I probably need to make some changes, but for now, I just leave it the way they suggest:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># /etc/schroot/schroot.conf</span>

[lucid]
description=Ubuntu Lucid
location=/var/chroot
priority=3
users=your_username
groups=sbuild
root-groups=root
</code></pre>
<p>Here's the command they give to install the operating system within the chroot:</p>
<pre><code class="lang-bash">sudo debootstrap --variant=buildd --arch i386 lucid /var/chroot/ http://mirror.url.com/ubuntu/
</code></pre>
<p>The instructions say I need to replace <code>mirror.url.com</code> with a valid mirror, so I find a Raspbian mirror online and give it a go:</p>
<pre><code class="lang-bash">sudo debootstrap --variant=buildd --arch i386 lucid /var/chroot/ http://raspbian.mirrors.lucidnetworks.net/raspbian/ <span class="hljs-comment"># press enter</span>

<span class="hljs-comment"># output -- error</span>
E: Failed getting release file http://raspbian.mirrors.lucidnetworks.net/raspbian/dists/lucid/Release
</code></pre>
<p>At first, I thought that I just needed to find a Raspbian mirror that had <code>/lucid</code> under the <code>/dists</code> directory. I wasn't finding one. Then I thought that changing <code>lucid</code> to <code>jammy</code> would work, because I thought I saw <code>jammy</code> somewhere...</p>
<p>So I tried it</p>
<pre><code class="lang-bash">sudo debootstrap --variant=buildd --arch i386 jammy /var/chroot/ https://mirror.vcu.edu/pub/gnu_linux/ubuntu/ <span class="hljs-comment"># press enter</span>

<span class="hljs-comment"># output -- error</span>
E: No such script: /usr/share/debootstrap/scripts/jammy
</code></pre>
<p>Even though it didn't work, at least it was a new error!</p>
<p>I decided to look at the contents of the mentioned <code>/scripts/</code> directory and see what was listed:</p>
<pre><code class="lang-bash">ls -la /usr/share/debootstrap/scripts <span class="hljs-comment"># press enter</span>

<span class="hljs-comment"># output</span>
drwxr-xr-x 2 root root 4096 May 16 06:40 .
drwxr-xr-x 3 root root 4096 May 16 06:40 ..
-rw-r--r-- 1 root root 6044 Jul 13  2022 aequorea
-rw-r--r-- 1 root root 6639 Jul 13  2022 amber
lrwxrwxrwx 1 root root    5 Jul 28  2022 artful -&gt; gutsy
lrwxrwxrwx 1 root root    3 Jul 28  2022 ascii -&gt; sid
lrwxrwxrwx 1 root root    8 Jul 28  2022 bartholomea -&gt; aequorea
lrwxrwxrwx 1 root root    3 Jul 28  2022 beowulf -&gt; sid
lrwxrwxrwx 1 root root    5 Jul 28  2022 bionic -&gt; gutsy
lrwxrwxrwx 1 root root    3 Jul 28  2022 bookworm -&gt; sid
-rw-r--r-- 1 root root 5404 Jul 13  2022 breezy
lrwxrwxrwx 1 root root    3 Jul 28  2022 bullseye -&gt; sid
<span class="hljs-comment"># ... &lt;omitted output&gt; ...</span>
</code></pre>
<p>Although <code>lucid</code> <em>is</em> listed here (not shown in the output), I stopped when I saw <code>bullseye</code> because I know that is what my Raspberry Pi is running.</p>
<p>I also did not realize that the different names (<code>lucid</code>, <code>bullseye</code>, etc.) corresponded to different versions of Debian. After looking into it a bit, I learned that <code>lucid</code> is quite old (Ubuntu 10.04), while <code>bullseye</code> is much newer (18.04 +).</p>
<p>With that, I change my configuration:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># /etc/schroot/schroot.conf</span>

[bullseye]
description=Raspbian Bullseye
location=/var/chroot
priority=3
users=sharif
groups=sbuild
root-groups=root
</code></pre>
<p>Changing <code>lucid</code> to <code>bullseye</code> is easy enough, but the mirror is a bit more involved. First, I need to find a <a target="_blank" href="https://www.debian.org/mirror/list">list of mirrors</a>, then find a mirror that is close to my geographical location.</p>
<p>I find the <a target="_blank" href="http://ftp.us.debian.org/debian/">United States mirror</a> and view its contents in the browser. From the error message I received previously, I know that the command looks for the version inside the <code>dists/</code> directory from the provided mirror URL.</p>
<p>I navigate to the <code>dists/</code> <a target="_blank" href="http://ftp.us.debian.org/debian/dists/">directory</a> within the browser, and see that <code>bullseye/</code> is listed.</p>
<p>Bingo!</p>
<p>So I go ahead and update my command with the correct version and mirror:</p>
<pre><code class="lang-bash">sudo debootstrap --variant=buildd --arch i386 bullseye /var/chroot/ http://ftp.us.debian.org/debian
</code></pre>
<p>It runs for a while and gets further than any of the previous attempts, but there's an issue at the very end:</p>
<pre><code class="lang-bash">W: Failure trying to run: chroot <span class="hljs-string">"/var/chroot"</span> /bin/<span class="hljs-literal">true</span>
W: See /var/chroot/debootstrap/debootstrap.log <span class="hljs-keyword">for</span> details
</code></pre>
<p>What happened here is that the <code>bullseye</code> version of Linux installed successfully within my <code>/var/chroot/</code> directory, but the system failed to open a shell within the chroot environment.</p>
<p>I follow the output instructions and view the contents of <code>debootstrap.log</code>. At the end of the log is a more detailed error:</p>
<p><code>chroot: failed to run command ‘/bin/true’: Exec format error</code></p>
<p>I Google this error, and the suggested fix is to:</p>
<ul>
<li><p>install the <code>qemu-user-static</code> package (required for user emulation)</p>
</li>
<li><p><code>sudo cp /usr/bin/qemu-arm-static /path/to/mount/usr/bin</code></p>
<ul>
<li><p>needed to adjust the path on the right side for my use case:</p>
</li>
<li><p><code>sudo cp /usr/bin/qemu-arm-static /var/chroot/</code></p>
</li>
</ul>
</li>
<li><p><code>systemctl restart systemd-binfmt.service</code></p>
</li>
</ul>
<p>Now I can try to launch the shell again:</p>
<pre><code class="lang-bash">sudo chroot /var/chroot <span class="hljs-comment"># press enter</span>

<span class="hljs-comment"># output</span>
bash: warning: setlocale: LC_ALL: cannot change locale (en_US.UTF-8)
I have no name!@raspberrypi:/<span class="hljs-comment">#</span>
</code></pre>
<p>I'm now logged in, but there's no user associated with the session:</p>
<pre><code class="lang-bash">whoami <span class="hljs-comment"># press enter</span>

<span class="hljs-comment"># output</span>
whoami: cannot find name <span class="hljs-keyword">for</span> user ID 0: No such file or directory
</code></pre>
<h2 id="heading-digging-deeper">Digging deeper</h2>
<p>Going through the above guide did not get me where I wanted to be. It was a good start, but I wasn't sure how to add a user in the chroot, or why it was launched without a user in the first place.</p>
<p>The guide also referenced some <code>mount</code> commands that I wasn't familiar with.</p>
<p>The end of the guide also mentioned using <code>schroot</code> instead of <code>chroot</code>, but I felt like I could use a bit more detail.</p>
<p>The last thing that spooked me a bit was that it seemed to imply that doing things incorrectly could result in the accidental erasure of data. That's not what I want.</p>
<p>So I decided to poke around a bit for other resources so that I could broaden my understanding. Some of the references in the initial guide seemed to assume I was more familiar with Linux than I am, so maybe I can find some more beginner-friendly material.</p>
<p>I found <a target="_blank" href="https://www.linode.com/docs/guides/use-chroot-for-testing-on-ubuntu/">this post</a>, which seems to make things a bit more simple, so I follow along with its example.</p>
<h2 id="heading-making-another-chroot">Making another chroot</h2>
<p>Following the Linode guide linked in the last section, I proceed to make a new chroot:</p>
<pre><code class="lang-bash">mkdir ~/chroot-jail <span class="hljs-comment"># press enter </span>

sudo debootstrap bullseye ~/chroot-jail <span class="hljs-comment"># press enter and wait for install</span>

sudo chroot ~/chroot-jail /bin/bash
</code></pre>
<p>This time when the chroot launches, I am the root user. I also didn't need to mess with any mirrors or config files. So far, this is much better.</p>
<p>But I still need to learn how to do things in the chroot. I need to explore what I'm able to do, and what I'm not able to do. I also need to figure out what the users of my app should and shouldn't be able to do.</p>
<p>Lots of work ahead.</p>
<p>Let's keep following the guide; it says to install <code>sudo</code>, but I seem to need to do some work before I can get there because I get this error when I run <code>apt install sudo</code>:</p>
<pre><code class="lang-bash">E: Can not write <span class="hljs-built_in">log</span> (Is /dev/pts mounted?) - posix_openpt (19: No such device)
</code></pre>
<p>Scrolling down in that section of the guide, I see that it instructs to mount the <code>/dev/</code> directory inside the chroot, along with some other directories:</p>
<pre><code class="lang-bash"><span class="hljs-built_in">exit</span> <span class="hljs-comment"># exit the chroot</span>

<span class="hljs-comment"># back in main operating system</span>
sudo mount --<span class="hljs-built_in">bind</span> /proc ~/chroot-jail/proc/
sudo mount --<span class="hljs-built_in">bind</span> /sys ~/chroot-jail/sys/
sudo mount --<span class="hljs-built_in">bind</span> /dev ~/chroot-jail/dev/

<span class="hljs-comment"># back into the chroot</span>
sudo chroot ~/chroot-jail /bin/bash
apt install sudo <span class="hljs-comment"># it works!</span>
</code></pre>
<p>This is better so far, but now I'm starting to see the issue behind the mount points. In this configuration, a user could alter or delete a file in the <code>/proc</code>, <code>/sys</code> or <code>/dev</code> directories, and it would impact the main OS.</p>
<p>I can see that the next section in the guide mentions using <code>schroot</code> instead of <code>chroot</code> to limit the user's access.</p>
<h2 id="heading-schroot"><code>schroot</code></h2>
<p>I want to prevent the chroot user from having root access, even inside the chroot, as we have just seen that it has the potential to be dangerous.</p>
<p>Following the guide:</p>
<pre><code class="lang-bash"><span class="hljs-built_in">exit</span> <span class="hljs-comment"># exit the chroot</span>

<span class="hljs-built_in">which</span> schroot <span class="hljs-comment"># make sure schroot is installed</span>
/usr/bin/schroot <span class="hljs-comment"># schroot is installed</span>

sudo vim /etc/schroot/schroot.conf <span class="hljs-comment"># edit the schroot.config file</span>

[bullseye]
description=Raspbian Bullseye
location=/home/sharif/chroot-jail
priority=3
users=sharif
groups=sbuild
root-groups=root

<span class="hljs-comment"># save the file and exit</span>

schroot -c bullseye <span class="hljs-comment"># run the schroot</span>

<span class="hljs-comment"># output</span>

W: line 87 [bullseye] priority: Could not parse value ‘3’
W: line 86 [bullseye]: Obsolete key ‘location’ used
I: This option has been removed, and no longer has any effect
W: Failed to change to directory ‘/home/sharif’: No such file or directory
I: The directory does not exist inside the chroot.  Use the --directory option to run the <span class="hljs-built_in">command</span> <span class="hljs-keyword">in</span> a di
fferent directory.
W: Falling back to directory ‘/’
W: Shell ‘/usr/bin/zsh’ not available: /usr/bin/zsh: Failed to <span class="hljs-built_in">stat</span> file: No such file or directory
W: Falling back to shell ‘/bin/bash’
</code></pre>
<p>Going through the output line by line, I can start to see what's happening:</p>
<ul>
<li><p><code>W: line 87 [bullseye] priority: Could not parse value ‘3’</code></p>
<ul>
<li>the <code>priority</code> line was leftover from the first guide I followed. I'll remove it</li>
</ul>
</li>
<li><p><code>W: line 86 [bullseye]: Obsolete key ‘location’ used</code></p>
</li>
<li><p><code>I: This option has been removed, and no longer has any effect</code></p>
<ul>
<li>looks like another issue from the previous config file. I see in the new guide that the key has been updated to <code>directory</code> instead of <code>location</code>, so I make that change</li>
</ul>
</li>
<li><p><code>W: Failed to change to directory ‘/home/sharif’: No such file or directory</code></p>
</li>
<li><p><code>I: The directory does not exist inside the chroot. Use the --directory option to run the command in a different directory.</code></p>
<ul>
<li><p>it looks like I misunderstood what I was specifying in the config file. I thought I was pointing to the directory that the chroot was under, but it instead wants the directory to load <em>inside the chroot</em></p>
</li>
<li><p>I assume I will need to first create a user inside the chroot, then point to that user's directory in the config file</p>
</li>
</ul>
</li>
<li><p><code>W: Falling back to directory ‘/’</code></p>
<ul>
<li>since there was no valid user for the chroot to load, we default to the root user directory</li>
</ul>
</li>
<li><p><code>W: Shell ‘/usr/bin/zsh’ not available: /usr/bin/zsh: Failed to stat file: No such file or directory</code></p>
</li>
<li><p><code>W: Falling back to shell ‘/bin/bash’</code></p>
<ul>
<li><p>I believe what happened here is that it tried to load <code>zsh</code> as my shell from the main operating system, because that is my user's default shell</p>
</li>
<li><p>This error should go away once I create a new user because their default shell will be <code>bash</code> and not <code>zsh</code></p>
</li>
</ul>
</li>
</ul>
<p>So the first thing I need to do is create a user within the chroot. I remember now that I had to follow the guide's instructions out of order, and that there were steps on creating a user in the guide:</p>
<pre><code class="lang-bash">adduser chroot-test sudo
-bash: adduser: <span class="hljs-built_in">command</span> not found

man useradd
-bash: man: <span class="hljs-built_in">command</span> not found

$ <span class="hljs-built_in">which</span> useradd
$ man useradd
-bash: man: <span class="hljs-built_in">command</span> not found
</code></pre>
<p>Looks like I'm not able to add users within the chroot. I'll try installing the package(s) it calls out:</p>
<pre><code class="lang-bash">$ apt-get install man
E: Could not open lock file /var/lib/dpkg/lock-frontend - open (13: Permission denied)
E: Unable to acquire the dpkg frontend lock (/var/lib/dpkg/lock-frontend), are you root?
$ <span class="hljs-built_in">pwd</span>
/
$ whoami
whoami: cannot find name <span class="hljs-keyword">for</span> user ID 1000
</code></pre>
<p>Ah, that's right. I'm still in the chroot that was launched with <code>schroot</code>, not the initial one I launched to install <code>sudo</code>. So I'll switch back to that:</p>
<pre><code class="lang-bash"><span class="hljs-built_in">exit</span> <span class="hljs-comment">#exit schroot</span>

sudo chroot ~/chroot-jail /bin/bash <span class="hljs-comment"># enter elevated chroot</span>

useradd -m chroot-user <span class="hljs-comment"># add user with home directory</span>

ls /home <span class="hljs-comment"># test to see if home directory was created</span>
<span class="hljs-comment"># output</span>
chroot-user

usermod -aG sudo chroot-user <span class="hljs-comment"># add user to sudo group</span>

groups chroot-user <span class="hljs-comment"># make sure sudo is listed</span>
<span class="hljs-comment">#output</span>
chroot-user : chroot-user sudo
</code></pre>
<p>Now I need to exit the chroot, go back to the <code>schroot</code> configuration file, and see if it works with my new user:</p>
<pre><code class="lang-bash"><span class="hljs-built_in">exit</span> <span class="hljs-comment"># exit chroot</span>
sudo vim /etc/schroot/schroot.conf <span class="hljs-comment"># edit the schroot.config file</span>

[bullseye]
description=Raspbian Bullseye
directory=/home/sharif/chroot-jail
users=chroot-user
groups=sbuild
root-groups=root

<span class="hljs-comment"># save and exit the file</span>

schroot -c bullseye

E: Access not authorised
I: You <span class="hljs-keyword">do</span> not have permission to access the schroot service
I: This failure will be reported.

sudo schroot -c bullseye

W: Failed to change to directory ‘/home/sharif’: No such file or directory
I: The directory does not exist inside the chroot.  Use the --directory option to run the <span class="hljs-built_in">command</span> <span class="hljs-keyword">in</span> a different directory.
W: Falling back to directory ‘/root’

<span class="hljs-built_in">exit</span> <span class="hljs-comment"># exit the chroot</span>

sudo vim /etc/schroot/schroot.conf <span class="hljs-comment"># edit the schroot.config file</span>

[bullseye]
description=Raspbian Bullseye
directory=/home/chroot-user
users=chroot-user
groups=sbuild
root-groups=root

<span class="hljs-comment"># save and exit the file</span>

sudo schroot -c bullseye
E: Failed to change to directory ‘/home/chroot-user’: No such file or directory
</code></pre>
<p>Seems to be some sort of circular logic here. I look back at the guide and see that it specifically says the user we are using in the chroot must match the user we are using to access the chroot.</p>
<p>I read this as: I need to also create a <code>chroot-user</code> in the actual operating system before I can use it in the chroot.</p>
<p>But, I'm tired, and sort of did the opposite by adding a <code>sharif</code> user to the chroot. Let's look at the effects of this mistake before correcting it:</p>
<pre><code class="lang-bash">sudo chroot ~/chroot-jail /bin/bash <span class="hljs-comment"># enter chroot</span>


useradd -m sharif

ls /home
chroot-user  sharif

groups sharif
sharif : sharif

sudo usermod -aG sudo sharif

<span class="hljs-built_in">exit</span> <span class="hljs-comment"># exit chroot</span>

sudo vim /etc/schroot/schroot.conf 

[bullseye]
description=Raspbian Bullseye
directory=/home/sharif/chroot-jail
users=sharif
groups=sbuild
root-groups=root

<span class="hljs-comment"># save and exit the file</span>

schroot -c bullseye <span class="hljs-comment"># enter chroot</span>

W: Shell ‘/usr/bin/zsh’ not available: /usr/bin/zsh: Failed to <span class="hljs-built_in">stat</span> file: No such file or directory
W: Falling back to shell ‘/bin/bash’
chroot-user@raspberrypi:~$ <span class="hljs-built_in">pwd</span>
/home/sharif
chroot-user@raspberrypi:~$ whoami
chroot-user
chroot-user@raspberrypi:~$ <span class="hljs-built_in">cd</span> ..
chroot-user@raspberrypi:/home$ ls
chroot-user  sharif
chroot-user@raspberrypi:/home$ <span class="hljs-built_in">cd</span> sharif/
chroot-user@raspberrypi:~$ ls -la
total 20
drwxr-xr-x 2 sharif sharif 4096 May 17 11:59 .
drwxr-xr-x 4 root   root   4096 May 17 11:59 ..
-rw-r--r-- 1 sharif sharif  220 Mar 27  2022 .bash_logout
-rw-r--r-- 1 sharif sharif 3526 Mar 27  2022 .bashrc
-rw-r--r-- 1 sharif sharif  807 Mar 27  2022 .profile
chroot-user@raspberrypi:~$ <span class="hljs-built_in">pwd</span>
/home/sharif
chroot-user@raspberrypi:~$ <span class="hljs-built_in">cd</span> ../chroot-user/
chroot-user@raspberrypi:/home/chroot-user$ ls -la
total 20
drwxr-xr-x 2 chroot-user chroot-user 4096 May 17 11:41 .
drwxr-xr-x 4 root        root        4096 May 17 11:59 ..
-rw-r--r-- 1 chroot-user chroot-user  220 Mar 27  2022 .bash_logout
-rw-r--r-- 1 chroot-user chroot-user 3526 Mar 27  2022 .bashrc
-rw-r--r-- 1 chroot-user chroot-user  807 Mar 27  2022 .profile
chroot-user@raspberrypi:/home/chroot-user$ <span class="hljs-built_in">exit</span>
</code></pre>
<p>So the problem is that I am logged into the chroot as the correct user, <code>chroot-user</code>, but there are some issues</p>
<ul>
<li><p>the chroot starts in the <code>/home/sharif</code> directory</p>
</li>
<li><p>the <code>sharif</code> user should not be on the system (I want a dedicated chroot for each user)</p>
</li>
</ul>
<p>The first logical step is to remove the <code>sharif</code> user from the chroot:</p>
<pre><code class="lang-bash">schroot -c bullseye
<span class="hljs-built_in">cd</span> /home/chroot-user

sudo userdel -r sharif

We trust you have received the usual lecture from the <span class="hljs-built_in">local</span> System
Administrator. It usually boils down to these three things:

    <span class="hljs-comment">#1) Respect the privacy of others.</span>
    <span class="hljs-comment">#2) Think before you type.</span>
    <span class="hljs-comment">#3) With great power comes great responsibility.</span>

[sudo] password <span class="hljs-keyword">for</span> chroot-user:
</code></pre>
<p>Oops, I never set a password when I created <code>chroot-user</code>. Let's see if I can fix that from within the chroot:</p>
<pre><code class="lang-bash">passwd chroot-user
Changing password <span class="hljs-keyword">for</span> chroot-user.
Current password:
</code></pre>
<p>Hm, I'm prompted for a password when trying to change the password. That won't do. Let me try in the original chroot:</p>
<pre><code class="lang-bash"><span class="hljs-built_in">exit</span>

sudo chroot ~/chroot-jail /bin/bash

ls /home
chroot-user  sharif

userdel -r sharif
userdel: sharif mail spool (/var/mail/sharif) not found

<span class="hljs-comment"># userdel gives an error, but the deletion still worked</span>

ls /home
chroot-user

<span class="hljs-comment"># try setting a password for chroot-user now as root</span>
passwd chroot-user
New password:
Retype new password:
passwd: password updated successfully

cat /etc/passwd <span class="hljs-comment"># confirm that user sharif is not listed</span>
</code></pre>
<p>Now I can do what I should have done originally -- create <code>chroot-user</code> on the base OS</p>
<pre><code class="lang-bash"><span class="hljs-built_in">exit</span> <span class="hljs-comment"># exit chroot</span>

sudo adduser chroot-user <span class="hljs-comment"># accept all default prompts</span>
sudo adduser chroot-user sudo
</code></pre>
<p>Now I need to adjust the <code>schroot</code> config again to make sure it points to the right user directory:</p>
<pre><code class="lang-bash">
sudo vim /etc/schroot/schroot.conf

[bullseye]
description=Raspbian Bullseye
directory=/home/sharif/chroot-jail
users=chroot-user <span class="hljs-comment"># the only line that was changed</span>
groups=sbuild
root-groups=root

<span class="hljs-comment"># save and exit the file</span>

schroot -c bullseye

<span class="hljs-comment"># output </span>
E: Access not authorised
I: You <span class="hljs-keyword">do</span> not have permission to access the schroot service.
I: This failure will be reported.

sudo schroot -c bullseye
W: Failed to change to directory ‘/home/sharif’: No such file or directory
I: The directory does not exist inside the chroot.  Use the --directory option to run the <span class="hljs-built_in">command</span> <span class="hljs-keyword">in</span> a different directory.
W: Falling back to directory ‘/root’
</code></pre>
<p>I think I finally understand the issue (at least partially).</p>
<p>It's trying to start the chroot from the base OS in the directory <code>/home/sharif/chroot-jail</code>, which does exist -- but that path (up until <code>/chroot-jail</code> also needs to be mirrored in the chroot OS, and since I removed the<code>sharif</code> user from the chroot, it no longer exists.</p>
<p>I believe the <code>directory</code> key will force the directory to be created on the base OS if it isn't already, but it can't create a folder in the chroot OS.</p>
<p>Time to try it out</p>
<pre><code class="lang-bash">sudo vim /etc/schroot/schroot.conf

[bullseye]
description=Raspbian Bullseye
directory=/home/chroot-user/chroot-jail <span class="hljs-comment"># changed this line</span>
users=chroot-user 
groups=sbuild
root-groups=root

<span class="hljs-comment"># save and exit the file</span>

schroot -c bullseye

E: Access not authorised
I: You <span class="hljs-keyword">do</span> not have permission to access the schroot service.
I: This failure will be reported.

sudo schroot -c bullseye

E: Failed to change to directory ‘/home/chroot-user/chroot-jail’: No such file or directory
</code></pre>
<p>Okay, so I think the issue is that I set up the <code>chroot-jail</code> folder under my <code>sharif</code> user, and I now need to move it to <code>chroot-user</code> for the above to work:</p>
<pre><code class="lang-bash">sudo mv /home/sharif/chroot-jail /home/chroot-user/

ls ~/ <span class="hljs-comment"># confirm chroot-user is no longer in my user's home directory</span>

ls /home/chroot-user
chroot-jail

schroot -c bullseye
E: Access not authorised
I: You <span class="hljs-keyword">do</span> not have permission to access the schroot service.
I: This failure will be reported.

sudo schroot -c bullseye
W: Failed to change to directory ‘/home/sharif’: No such file or directory
I: The directory does not exist inside the chroot.  Use the --directory option to run the <span class="hljs-built_in">command</span> <span class="hljs-keyword">in</span> a different directory.
</code></pre>
<p>And now I am back to being confused. I am not sure why the chroot is trying to access <code>/home/sharif</code> because it is not referenced in the configuration file.</p>
<p>I source my shell, then get a more fitting error message:</p>
<pre><code class="lang-bash"><span class="hljs-built_in">source</span> ~/.zshrc

sudo schroot -c bullseye

E: Failed to change to directory ‘/home/chroot-user/chroot-jail ’: No such file or directory

ls /home/chroot-user
chroot-jail
</code></pre>
<p>The folder looks like it's there. so maybe it's a permissions issue?</p>
<pre><code class="lang-bash"> ls -la /home/chroot-user

drwxr-xr-x  3 chroot-user chroot-user 4096 May 17 09:55 .
drwxr-xr-x  5 root        root        4096 May 17 09:41 ..
-rw-r--r--  1 chroot-user chroot-user  220 May 17 09:41 .bash_logout
-rw-r--r--  1 chroot-user chroot-user 3523 May 17 09:41 .bashrc
drwxr-xr-x 17 root        root        4096 May 17 05:48 chroot-jail
</code></pre>
<p>Maybe it's because the chroot-jail folder is owned by root?</p>
<pre><code class="lang-bash">sudo chown chroot-user:chroot-user /home/chroot-user/chroot-jail

ls -la /home/chroot-user

drwxr-xr-x  3 chroot-user chroot-user 4096 May 17 09:55 .
drwxr-xr-x  5 root        root        4096 May 17 09:41 ..
-rw-r--r--  1 chroot-user chroot-user  220 May 17 09:41 .bash_logout
-rw-r--r--  1 chroot-user chroot-user 3523 May 17 09:41 .bashrc
drwxr-xr-x 17 chroot-user chroot-user 4096 May 17 05:48 chroot-jail
-rw-r--r--  1 chroot-user chroot-user 1670 May 17 09:41 .mkshrc
-rw-r--r--  1 chroot-user chroot-user  807 May 17 09:41 .profile

schroot -c bullseye
E: Access not authorised
I: You <span class="hljs-keyword">do</span> not have permission to access the schroot service.
I: This failure will be reported.

sudo schroot -c bullseye
E: Failed to change to directory ‘/home/chroot-user/chroot-jail ’: No such file or directory

sudo vim /etc/schroot/schroot.conf 

<span class="hljs-comment"># change directory to /home/chroot-user (take off '/chroot-jail')</span>

schroot -c bullseye

E: Access not authorised
I: You <span class="hljs-keyword">do</span> not have permission to access the schroot service.
I: This failure will be reported.

sudo schroot -c bullseye

W: Failed to change to directory ‘/home/sharif’: No such file or directory
I: The directory does not exist inside the chroot.  Use the --directory option to run the <span class="hljs-built_in">command</span> <span class="hljs-keyword">in</span> a different directory.
W: Failed to change to directory ‘/root’: No such file or directory
I: The directory does not exist inside the chroot.  Use the --directory option to run the <span class="hljs-built_in">command</span> <span class="hljs-keyword">in</span> a different directory.
W: Falling back to directory ‘/’
W: Shell ‘/bin/bash’ not available: /bin/bash: Failed to <span class="hljs-built_in">stat</span> file: No such file or directory
W: Shell ‘/bin/sh’ not available: /bin/sh: Failed to <span class="hljs-built_in">stat</span> file: No such file or directory
W: Falling back to shell ‘/bin/sh’
E: Failed to execute “/bin/sh”: No such file or directory
</code></pre>
<p>Now I get even more errors! My understanding of the configuration needs a little work. One thing that I can say is that it seems related to file permissions between the base OS and the chroot.</p>
<p>I decide to look up the documentation for <code>schroot</code>, and it looks like I might need to specify my <code>chroot</code> type as <code>directory</code> :</p>
<pre><code class="lang-bash"><span class="hljs-comment"># /etc/schroot/schroot.conf</span>
[bullseye]
description=Raspbian Bullseye
<span class="hljs-built_in">type</span>=directory <span class="hljs-comment"># added this line</span>
directory=/home/chroot-user/chroot-jail <span class="hljs-comment"># changed this line</span>
users=chroot-user
groups=sbuild
root-groups=root
<span class="hljs-comment"># save the file</span>

schroot -c bullseye
E: Access not authorised
I: You <span class="hljs-keyword">do</span> not have permission to access the schroot service.
I: This failure will be reported.

sudo schroot -c bullseye
</code></pre>
<p>I know that I should be able to run the <code>schroot -c bullseye</code> command without elevated permissions. When I get an error, I know that there is something amiss with the configuration. However, when I run it with <code>sudo</code>, I typically get some more information about why the command failed.</p>
<p>This time, when I run it with <code>sudo</code>, I get no errors, but I still don't get the desired behavior. For one, it puts me in the <code>/home/sharif</code> directory on the base OS, not the chroot. Even worse, when I run a <code>whoami</code>, I get back <code>root</code>.</p>
<p>I need to understand why this is happening. I think I will try un-commenting one of the more basic-looking configs in the <code>schroot.conf</code> file and see what happens:</p>
<pre><code class="lang-bash">[sid]
description=Debian sid (unstable)
directory=/srv/chroot/sid
users=rleigh <span class="hljs-comment"># changing this to 'chroot-user'</span>
groups=sbuild
root-groups=root
aliases=unstable,default
<span class="hljs-comment"># save the file</span>

schroot -c sid

E: Access not authorised
I: You <span class="hljs-keyword">do</span> not have permission to access the schroot service.
I: This failure will be reported.

sudo schroot -c sid
E: Failed to change to directory ‘/srv/chroot/sid’: No such file or directory
</code></pre>
<p>Running the command with <code>sudo</code> gives an error that the directory doesn't exist, so I create it and try again:</p>
<pre><code class="lang-bash">sudo mkdir -p /srv/chroot/sid

sudo schroot -c sid

W: Failed to change to directory ‘/home/sharif’: No such file or directory
I: The directory does not exist inside the chroot.  Use the --directory option to run the <span class="hljs-built_in">command</span> <span class="hljs-keyword">in</span> a different directory.
W: Failed to change to directory ‘/root’: No such file or directory
I: The directory does not exist inside the chroot.  Use the --directory option to run the <span class="hljs-built_in">command</span> <span class="hljs-keyword">in</span> a different directory.
W: Falling back to directory ‘/’
W: Shell ‘/bin/bash’ not available: /bin/bash: Failed to <span class="hljs-built_in">stat</span> file: No such file or directory
W: Shell ‘/bin/sh’ not available: /bin/sh: Failed to <span class="hljs-built_in">stat</span> file: No such file or directory
W: Falling back to shell ‘/bin/sh’
E: Failed to execute “/bin/sh”: No such file or directory
</code></pre>
<p>So, this is strange... even though there is no reference to <code>/home/sharif</code> in the config file, it is looking for that directory in the chroot. Did I maybe mess something up with the <code>chroot-user</code> ?</p>
<p>I decide to look back at the <a target="_blank" href="https://www.linode.com/docs/guides/use-chroot-for-testing-on-ubuntu/">guide</a> I was originally following, and start fresh. Maybe I will notice something that I didn't before, now that I have a bit more experience with it.</p>
<p>I start by deleting the <code>chroot-jail</code> folder that I tried to assign to the <code>chroot-user</code>, delete the user, recreate the <code>chroot-jail</code> folder, and reinstall the chroot OS there:</p>
<pre><code class="lang-bash">sudo rm -rf /home/chroot-user/chroot-jail

sudo userdel -r chroot-user

mkdir ~/chroot-jail

sudo debootstrap bullseye ~/chroot-jail
</code></pre>
<p>The install runs for a while, but ends with an error when trying to run the chroot:</p>
<pre><code class="lang-bash">W: Failure trying to run: chroot <span class="hljs-string">"/home/sharif/chroot-jail"</span> dpkg --force-overwrite --force-confold --skip-same-version -
-install /var/cache/apt/archives/libapparmor1_2.13.6-10_armhf.deb /var/cache/apt/archives/libargon2-1_0~20171227-0.2_arm
hf.deb /var/cache/apt/archives/libcryptsetup12_2%3a2.3.7-1+deb11u1_armhf.deb /var/cache/apt/archives/libip4tc2_1.8.7-1_a
rmhf.deb /var/cache/apt/archives/libjson-c5_0.15-2_armhf.deb /var/cache/apt/archives/libkmod2_28-1_armhf.deb /var/cache/
apt/archives/libcap2_1%3a2.44-1_armhf.deb /var/cache/apt/archives/dmsetup_2%3a1.02.175-2.1_armhf.deb /var/cache/apt/arch
ives/libdevmapper1.02.1_2%3a1.02.175-2.1_armhf.deb /var/cache/apt/archives/systemd_247.3-7+deb11u2_armhf.deb

W: See /home/sharif/chroot-jail/debootstrap/debootstrap.log <span class="hljs-keyword">for</span> details (possibly the package systemd is at fault)
</code></pre>
<p>The first message is a bit tough to unpack, so I go to the second, which gives me a file path for a log that I can view:</p>
<pre><code class="lang-bash">cat /home/sharif/chroot-jail/debootstrap/debootstrap.log <span class="hljs-comment"># press enter</span>

<span class="hljs-comment"># last part of output:</span>
dpkg: error processing package systemd (--install):
 installed systemd package post-installation script subprocess returned error <span class="hljs-built_in">exit</span> status 73

Processing triggers <span class="hljs-keyword">for</span> libc-bin (2.31-13+deb11u6) ...
Errors were encountered <span class="hljs-keyword">while</span> processing:
 systemd
</code></pre>
<p>The <code>dpkg</code> error seems more specific, so I start by googling that.</p>
<p>I end up on <a target="_blank" href="https://askubuntu.com/questions/1244976/how-to-fix-dpkg-error-processing-package-systemd-configure">this post</a>, and give the accepted answer a try:</p>
<pre><code class="lang-bash"><span class="hljs-built_in">stat</span> / /dev /var <span class="hljs-comment"># press enter</span>
</code></pre>
<p>It looks like <code>root</code> is the owner of all those directories, so I believe my problem is a different one. The post above gives credit to <a target="_blank" href="https://askubuntu.com/questions/1171668/error-processing-package-apt-upgrade-cannot-run-due-to-package-configuration/1215142#1215142">another post</a>, which has another answer that includes removing <code>/var/lib/dpkg/info/systemd*</code>. Somebody in the comments said this fixed the problem, but now they have warnings about missing files when they use <code>apt-get</code> to install something. I'm not sure if I like this solution.</p>
<p>I decide to go back to my Google search results. Another post suggests running <code>sudo apt update</code>:</p>
<pre><code class="lang-bash">sudo apt update
</code></pre>
<p>Now I should be able to try and run the chroot again since the install worked; the problem is with launching the chroot. Time to see if the update fixed it:</p>
<pre><code class="lang-bash">sudo chroot ~/chroot-jail /bin/bash
</code></pre>
<p>This works, but now the problem is that I'm using <code>chroot</code>, which gives full access to the chroot, when I need to be using <code>schroot</code> for limited access.</p>
<p>I continue following the instructions in the <a target="_blank" href="https://www.linode.com/docs/guides/use-chroot-for-testing-on-ubuntu/">guide</a>:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># in the chroot</span>
adduser sharif<span class="hljs-comment"># go through the prompts to set a password and info</span>
adduser sharif sudo

<span class="hljs-comment"># exit chroot</span>
<span class="hljs-built_in">exit</span>

<span class="hljs-comment"># mount per instructions</span>
sudo mount --<span class="hljs-built_in">bind</span> /proc ~/chroot-jail/proc/
sudo mount --<span class="hljs-built_in">bind</span> /sys ~/chroot-jail/sys/
sudo mount --<span class="hljs-built_in">bind</span> /dev ~/chroot-jail/dev/

<span class="hljs-comment"># /etc/schroot/schroot.conf</span>
[bullseye]
description=Raspbian Bullseye
directory=/home/sharif/chroot-jail
users=sharif
groups=sbuild
root-groups=root
aliases=bullseye
<span class="hljs-comment"># save the file</span>

<span class="hljs-comment"># run schroot</span>
schroot -c bullseye
</code></pre>
<p>It works! The issue was that the user you are launching the command as in the base operating system must match the user in the chroot. I was only halfway there before since I had a matching user in the chroot, but I was not executing the command <em>as that user</em> from the base OS.</p>
<p>Now, the issue is that I don't think the <code>sharif</code> user in the chroot should have root privileges, but my <code>sharif</code> user in the base OS <em>does</em> have root privileges. I think I need to do this again, but with a non-elevated user on both sides:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># remove the chroot-jail folder for my sharif user</span>
sudo rm -rf ~/chroot-jail

<span class="hljs-comment"># output gives a bunch of 'Permission denied' errors</span>
</code></pre>
<p>Ah, that's right. I need to follow the <a target="_blank" href="https://www.linode.com/docs/guides/use-chroot-for-testing-on-ubuntu/#exit-and-remove-a-chroot-environment">instructions for removing a chroot</a>.</p>
<pre><code class="lang-bash">sudo umount ~/chroot-jail/dev
sudo umount ~/chroot-jail/sys
sudo umount ~/chroot-jail/proc

<span class="hljs-comment"># output</span>
git_prompt_info:3: permission denied: /dev/null
<span class="hljs-comment"># end output</span>
sudo rm -R ~/chroot-jail

git_prompt_info:3: permission denied: /dev/null
</code></pre>
<p>It looks like the removal worked, but I'm not sure what the error is for, so back to Google..</p>
<p>Looks like rebooting the system will do the trick:</p>
<pre><code class="lang-bash">sudo reboot
</code></pre>
<p>Now I need to create the non-root user in the base OS, install the chroot jail under <em>their user</em> this time, then add the user in the chroot:</p>
<pre><code class="lang-bash">sudo adduser schrooty <span class="hljs-comment"># press enter</span>

<span class="hljs-comment"># go through propts to set password, name etc.</span>

<span class="hljs-comment"># switch users</span>
su - schrooty <span class="hljs-comment"># press enter</span>
<span class="hljs-comment"># enter password</span>

<span class="hljs-comment"># follow chroot instructions</span>
mkdir ~/chroot-jail
</code></pre>
<p>And it is at this point that I realize that I was incorrect in my thought process. Following the instructions for setting up the chroot, <code>sudo</code> is required. That means that I either need to:</p>
<ul>
<li><p>run the commands as my <code>sharif</code> user in the base OS, and point to <code>/home/schrooty/root-jail</code> when I install and run <code>chroot</code></p>
</li>
<li><p>create another user that I add to <code>sudo</code>, which will be used for managing the chroot (I don't want to manage it with my normal user account that I use for everything else)</p>
</li>
</ul>
<p>It's also worth calling out that I'm not sure if the chroot user needs to own the <code>chroot-jail</code> folder, or if it needs to be owned by root. I assume it needs to be owned by the chroot user, as the instructions do not say to run the <code>mkdir</code> command with <code>sudo</code></p>
<p>It may also be the case that the folder needs to be owned by whatever user is spawning the chroot.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># switch back to sharif user</span>
su - sharif <span class="hljs-comment"># press enter</span>
<span class="hljs-comment"># enter password</span>

<span class="hljs-comment"># follow instructions, but pointing to schrooty</span>
sudo debootstrap bullseye /home/schrooty/chroot-jail

<span class="hljs-comment"># output</span>
W: Failure trying to run: chroot <span class="hljs-string">"/home/schrooty/chroot-jail"</span> dpkg --force-overwrite --force-confold --skip-same-version --install /var/cache/apt/archives/libapparmor1_2.13.6-10_armhf.deb /var/cache/apt/archives/libargon2-1_0~20171227-0.2_armhf.deb /var/cache/apt/archives/libcryptsetup12_2%3a2.3.7-1+deb11u1_armhf.deb /var/cache/apt/archives/libip4tc2_1.8.7-1_armhf.deb /var/cache/apt/archives/libjson-c5_0.15-2_armhf.deb /var/cache/apt/archives/libkmod2_28-1_armhf.deb /var/cache/apt/archives/libcap2_1%3a2.44-1_armhf.deb /var/cache/apt/archives/dmsetup_2%3a1.02.175-2.1_armhf.deb /var/cache/apt/archives/libdevmapper1.02.1_2%3a1.02.175-2.1_armhf.deb /var/cache/apt/archives/systemd_247.3-7+deb11u2_armhf.deb

W: See /home/schrooty/chroot-jail/debootstrap/debootstrap.log <span class="hljs-keyword">for</span> details (possibly the package systemd is at fault)
</code></pre>
<p>It's the same error I got last time. I guess running the update didn't do anything. I decide to ignore the warning and try to run the chroot:</p>
<pre><code class="lang-bash">sudo chroot /home/schrooty/chroot-jail /bin/bash <span class="hljs-comment"># press enter</span>

<span class="hljs-comment"># successfully launched chroot</span>
<span class="hljs-comment"># now in chroot</span>
adduser schrooty <span class="hljs-comment"># press enter</span>
<span class="hljs-comment"># go through prompts to create user and set password</span>
<span class="hljs-comment"># exit chroot</span>
<span class="hljs-built_in">exit</span>

<span class="hljs-comment"># back in base OS (as user sharif)</span>
sudo mount --<span class="hljs-built_in">bind</span> /proc /home/schrooty/chroot-jail/proc/
sudo mount --<span class="hljs-built_in">bind</span> /sys /home/schrooty/chroot-jail/sys/
sudo mount --<span class="hljs-built_in">bind</span> /dev /home/schrooty/chroot-jail/dev/

<span class="hljs-comment"># configure schroot</span>
sudo vim /etc/schroot/schroot.conf

[bullseye]
description=Raspbian Bullseye
directory=/home/schrooty/chroot-jail
users=schrooty
groups=sbuild
root-groups=root
aliases=bullseye
<span class="hljs-comment"># save and exit the file</span>

<span class="hljs-comment"># attempt to launch schroot</span>
schroot -c bullseye
W: line 91 [bullseye] aliases: Alias ‘bullseye’ already associated with ‘unknown’ chroot
E: Access not authorised
I: You <span class="hljs-keyword">do</span> not have permission to access the schroot service.
I: This failure will be reported.

<span class="hljs-comment"># is the problem with the alias?</span>
sudo vim /etc/schroot/schroot.conf
<span class="hljs-comment"># remove alias and save file</span>

<span class="hljs-comment"># nope, didn't fix it</span>
schroot -c bullseye
E: Access not authorised
I: You <span class="hljs-keyword">do</span> not have permission to access the schroot service.
I: This failure will be reported.

<span class="hljs-comment"># try with sudo</span>
sudo schroot -c bullseye
W: Failed to change to directory ‘/home/sharif’: No such file or directory
I: The directory does not exist inside the chroot.  Use the --directory option to run the <span class="hljs-built_in">command</span> <span class="hljs-keyword">in</span> a different directory.
W: Falling back to directory ‘/root’

<span class="hljs-comment"># maybe I need to run this as schrooty?</span>
<span class="hljs-comment"># exit chroot (since it launched as root -- not what I want)</span>
<span class="hljs-built_in">exit</span>
su - schrooty
schroot -c bullseye <span class="hljs-comment"># success!</span>
</code></pre>
<p>It works this time, but there are some issues:</p>
<ul>
<li><p>the prompt shows up as <code>I have no name!@raspberrypi</code></p>
</li>
<li><p>running <code>groups</code> yields: <code>groups: cannot find name for group ID 1003</code></p>
</li>
<li><p>running <code>whoami</code> yields: <code>whoami: cannot find name for user ID 1002</code></p>
</li>
</ul>
<p>However, the chroot did launch in the correct directory: <code>/home/schrooty</code>. The problem seems to be that it's not launching the session <em>as</em> schrooty.</p>
<p>After looking online, I learn that is not the reason for this behavior. It is because files like <code>/etc/passwd</code> and <code>/etc/shadow</code> are not present in the chroot, so they need to be mounted like the other directories from the config instructions.</p>
<p>I will keep <a target="_blank" href="https://comp.os.linux.misc.narkive.com/0aXUVPjV/bash-in-chroot-i-have-no-name">this post</a> as reference, as the answer was given by the author of the <code>schrooty</code> package</p>
<pre><code class="lang-bash"><span class="hljs-comment"># exit chroot</span>
<span class="hljs-built_in">exit</span>

<span class="hljs-comment"># switch back to sharif user so I can use sudo</span>
su - sharif 
<span class="hljs-comment"># enter password</span>

<span class="hljs-comment"># mount files</span>
sudo mount --<span class="hljs-built_in">bind</span> /etc/passwd /home/schrooty/chroot-jail/etc/passwd
sudo mount --<span class="hljs-built_in">bind</span> /etc/shadow /home/schrooty/chroot-jail/etc/shadow

<span class="hljs-comment"># switch back to schrooty</span>
su - schrooty
<span class="hljs-comment"># enter password</span>

<span class="hljs-comment"># launch chroot</span>
schroot -c bullseye

<span class="hljs-comment"># prompt now has username -- schrooty@raspberrypi:~ $</span>
whoami
<span class="hljs-comment"># output</span>
schrooty

groups
<span class="hljs-comment">#output</span>
groups: cannot find name <span class="hljs-keyword">for</span> group ID 1003
</code></pre>
<p>Much better! Except I now need to copy the file for groups over to the chroot:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># exit chroot</span>
<span class="hljs-built_in">exit</span>

<span class="hljs-comment"># switch users</span>
su - sharif

<span class="hljs-comment"># mount file</span>
sudo mount --<span class="hljs-built_in">bind</span> /etc/group /home/schrooty/chroot-jail/etc/group

<span class="hljs-comment"># switch users</span>
su - schrooty

<span class="hljs-comment"># luanch chroot</span>
schroot -c bullseye

<span class="hljs-built_in">pwd</span> <span class="hljs-comment"># outputs /home/schrooty</span>
whoami <span class="hljs-comment"># outputs schrooty</span>
groups <span class="hljs-comment"># outputs schrooty</span>
</code></pre>
<p>After <em>alllll</em> of this, I decided to look into how secure <code>chroot</code> (even with <code>schroot</code>) actually is...</p>
<p>And it's not good. It looks <a target="_blank" href="https://benjamintoll.com/2019/05/18/on-escaping-a-chroot/">relatively easy to exploit</a>.</p>
<p>It looks like a Docker container is going to give me what I'm after, but I still think this was a good exercise.</p>
<p>So, stay tuned for the Docker version of this one I guess 😅</p>
]]></content:encoded></item><item><title><![CDATA[Locking down root privileges on a cloud instance]]></title><description><![CDATA[When I was writing my last blog post, I ended up having to enable root access to my Vultr instance so that I could reset my SSH configuration and document it properly for the blog post.
But now I'm done with that blog post, and root is still availabl...]]></description><link>https://blog.saleshorse.org/locking-down-root-privileges-on-a-cloud-instance</link><guid isPermaLink="true">https://blog.saleshorse.org/locking-down-root-privileges-on-a-cloud-instance</guid><category><![CDATA[Cloud]]></category><category><![CDATA[Linux]]></category><category><![CDATA[Security]]></category><dc:creator><![CDATA[Sharif Elkassed]]></dc:creator><pubDate>Tue, 16 May 2023 00:11:43 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/DJVI3Q4DLAw/upload/0640261a2ef47b948e0d1c7c0ac4eac7.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When I was writing <a target="_blank" href="https://blog.saleshorse.org/managing-multiple-ssh-keys-on-both-ends">my last blog post</a>, I ended up having to enable <code>root</code> access to my Vultr instance so that I could reset my SSH configuration and document it properly for the blog post.</p>
<p>But now I'm done with that blog post, and <code>root</code> is still available for login with a password (I didn't use my real domain name in that blog post, calm down).</p>
<p>Naturally, I'd like to lock it back down to the status it was in before I wrote that blog post. I figured I'd document the process and share my findings along the way.</p>
<h2 id="heading-prework">Prework</h2>
<p>Before disabling <code>root</code> login on the Vultr instance, I need to make sure that I'm able to ssh in as a non-root user.</p>
<p>This is because some <code>ssh</code> configuration has to be done as the <code>root</code> user on the Vultr instance. Once that configuration is done, then we can be done with <code>root</code>.</p>
<h3 id="heading-vultr-documentation">Vultr documentation</h3>
<p>Referencing <a target="_blank" href="https://www.vultr.com/docs/using-your-ssh-key-to-login-to-non-root-users">Vultr's documentation for this use case</a>, they present three options:</p>
<ul>
<li><p>create a new SSH key</p>
<ul>
<li>not a fan of this because getting the key from the server to my local machine is more involved than the other way around</li>
</ul>
</li>
<li><p>move the root SSH key to the non-root user</p>
<ul>
<li>this immediately jumped out as the approach I wanted to use</li>
</ul>
</li>
<li><p>startup scripts</p>
<ul>
<li><p>seems too involved</p>
</li>
<li><p>docs say it's best suited for deploying many instances</p>
</li>
</ul>
</li>
</ul>
<p>So I proceed with option #2: move the root SSH key to the non-root user</p>
<h2 id="heading-check-existing-keys">Check existing keys</h2>
<p>The Vultr instructions say to</p>
<ul>
<li><p>make a <code>~/.ssh</code> folder for the non-root user</p>
</li>
<li><p>move the <code>authorized_keys</code> file from <code>/root/.ssh/</code> to <code>/home/&lt;nonRootUser&gt;/.ssh/</code></p>
</li>
<li><p>make the non-root user the owner of the <code>/home/&lt;nonRootUser/.ssh</code> directory</p>
</li>
</ul>
<p>...but I think I may have already done some of these steps.</p>
<p>So I log into the Vultr instance as <code>root</code>, and start poking around:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># root@vultr</span>

ls -la ~/.ssh <span class="hljs-comment"># press enter</span>

<span class="hljs-comment"># output</span>
drwx------  2 root  wheel  512 May 14 16:15 .
drwx------  3 root  wheel  512 May 14 15:54 ..
-rw-------  1 root  wheel  185 May 14 19:34 authorized_keys
</code></pre>
<p>I can see that <code>/root/authorized_keys</code> exists, so I see what's in it:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># root@vultr</span>

cat /root/.ssh/authorized_keys <span class="hljs-comment"># press enter</span>

<span class="hljs-comment"># output</span>
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEbll4kwbvR/um3/zzMxEZs7+Jmj8NpAPkqQj8SC6cia idev@MSI
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICaQ9obHa6yYVZPqjQSaeG5OadwxxYguPJVNA7zJpuEm jester@devbox
</code></pre>
<p>I see that there are two public keys, one from each of my laptops. I believe these are the keys that I generated and copied over in the last blog post.</p>
<p>To confirm these are the correct keys, I go to each client machine:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># idev@MSI</span>

ls -la ~/.ssh <span class="hljs-comment"># press enter</span>

<span class="hljs-comment"># output</span>
drwx------  2 idev idev 4096 May 14 12:15 .
drwxr-x--- 34 idev idev 4096 May 15 18:18 ..
-rw-------  1 idev idev 1342 May 14 11:30 known_hosts
-rw-------  1 idev idev 1120 May 14 11:30 known_hosts.old
-rw-------  1 idev idev  399 May 14 12:11 vultr_ed25519
-rw-r--r--  1 idev idev   90 May 14 12:11 vultr_ed25519.pub
</code></pre>
<p>I see what looks like a Vultr public key, so I check its contents:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># idev@MSI</span>

cat ~/.ssh/vultr_ed25519.pub <span class="hljs-comment"># press enter</span>

<span class="hljs-comment"># output</span>
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEbll4kwbvR/um3/zzMxEZs7+Jmj8NpAPkqQj8SC6cia idev@MSI
</code></pre>
<p>I can see that the public key on the client machine matches what is in the <code>/root/authorized_keys</code> file on the Vultr instance. So far so good.</p>
<p>Now I do the same thing on my other laptop:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># jester@devbox</span>

ls -la ~/.ssh <span class="hljs-comment"># press enter</span>

<span class="hljs-comment"># output</span>
drwx------  2 jester jester 4096 May 14 19:37 .
drwxr-x--- 21 jester jester 4096 May 15 18:26 ..
-rw-------  1 jester jester  201 May 14 19:37 config
-rw-------  1 jester jester 1484 May  2 17:22 known_hosts
-rw-------  1 jester jester  506 Apr 29 12:41 known_hosts.old
-rw-------  1 jester jester  399 May 14 19:29 rpi_ed25519
-rw-r--r--  1 jester jester   95 May 14 19:29 rpi_ed25519.pub
-rw-------  1 jester jester  399 May 14 15:33 vultr_ed25519
-rw-r--r--  1 jester jester   95 May 14 15:33 vultr_ed25519.pub
</code></pre>
<p>I see an additional key pair and a config file, but right now I'm just concerned with the Vultr key, so I view the contents of the file to see if it matches what's on the server:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># jester@devbox</span>

cat ~/.ssh/vultr_ed25519.pub <span class="hljs-comment"># press enter</span>

<span class="hljs-comment"># output</span>
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICaQ9obHa6yYVZPqjQSaeG5OadwxxYguPJVNA7zJpuEm jester@devbox
</code></pre>
<p>It does indeed match, so I'm ready to move on to the next step.</p>
<h3 id="heading-check-the-non-root-user">Check the non-root user</h3>
<p>Recall that the instructions said to make a <code>.ssh</code> folder for the non-root user, but I think I already created it, so I go ahead and check:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># root@vultr</span>

ls -la /home/sharif/.ssh <span class="hljs-comment"># press enter</span>

<span class="hljs-comment"># output</span>
drwx------  2 sharif  sharif  512 May 14 15:01 .
drwxr-xr-x  4 sharif  sharif  512 May 14 14:55 ..
-rw-------  1 sharif  sharif  747 May 14 15:04 authorized_keys
</code></pre>
<p>Yep, there it is. I suspect the <code>authorized_keys</code> file is not up-to-date because of the configuration changes I made during my last blog post, so I view the contents:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># root@vultr</span>

cat /home/sharif/.ssh/authorized_keys
</code></pre>
<p>Sure enough, I've got some old keys that don't match what's on the client machines, so I remove the file:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># root@vultr</span>

rm /home/sharif/.ssh/authorized_keys
</code></pre>
<p>Now I can follow Vultr's instructions and move the <code>root</code> user's <code>authorized_keys</code> file over to my non-root user:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># root@vultr</span>

mv /root/.ssh/authorized_keys /home/sharif/.ssh/ <span class="hljs-comment"># press enter</span>

<span class="hljs-comment"># make sure it worked</span>

ls -la /home/sharif/.ssh/ <span class="hljs-comment"># press enter</span>

<span class="hljs-comment"># output </span>
drwx------  2 sharif  sharif  512 May 15 22:43 .
drwxr-xr-x  4 sharif  sharif  512 May 14 14:55 ..
-rw-------  1 root    wheel   185 May 14 19:34 authorized_keys
<span class="hljs-comment"># end output</span>

cat /home/sharif/.ssh/authorized_keys <span class="hljs-comment"># press enter</span>

<span class="hljs-comment"># output -- correct keys confirmed</span>
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEbll4kwbvR/um3/zzMxEZs7+Jmj8NpAPkqQj8SC6cia idev@MSI
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICaQ9obHa6yYVZPqjQSaeG5OadwxxYguPJVNA7zJpuEm jester@devbox
</code></pre>
<p>Now to adjust the permissions so that the non-root user can properly access the <code>authorized_keys</code> file:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># root@vultr</span>

chown -R sharif:sharif /home/sharif/.ssh <span class="hljs-comment"># press enter</span>

<span class="hljs-comment"># make sure it worked</span>
ls -la /home/sharif/.ssh <span class="hljs-comment"># press enter</span>

<span class="hljs-comment"># output -- non-root ownership confirmed</span>
drwx------  2 sharif  sharif  512 May 15 22:43 .
drwxr-xr-x  4 sharif  sharif  512 May 14 14:55 ..
-rw-------  1 sharif  sharif  185 May 14 19:34 authorized_keys
</code></pre>
<p>The non-root user's configuration looks good; now to test it from both clients.</p>
<h2 id="heading-test-ssh-as-the-non-root-user">Test SSH as the non-root user</h2>
<p>This part should (hopefully) be pretty painless since I set up SSH configuration files in the last blog post.</p>
<p>Theoretically, I should just be able to change the user from <code>root</code> to <code>sharif</code> in the config, use that config for both laptops, and all should be well.</p>
<p>Let's try it:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># idev@MSI</span>

<span class="hljs-comment"># /home/idev/.ssh/config</span>
Host myvultrserver.com
    HostName myvultrserver.com
    User sharif
    IdentityFile ~/.ssh/vultr_ed25519
</code></pre>
<p>Moment of truth:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># idev@MSI</span>
ssh myvultrserver.com
</code></pre>
<p>Success!</p>
<p>The process should be the same on my other laptop:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># jester@devbox</span>

<span class="hljs-comment"># /home/jester/.ssh/config</span>
Host myvultrserver.com
    HostName myvultrserver.com
    User sharif
    IdentityFile ~/.ssh/vultr_ed25519

<span class="hljs-comment"># save the config file</span>

<span class="hljs-comment"># test ssh</span>
ssh myvultrserver.com
</code></pre>
<p>Now that I've confirmed I can SSH into the server with both my client machines (laptops), I can finally move on to removing the ability to:</p>
<ul>
<li><p>log in as root</p>
</li>
<li><p>log in with a password</p>
</li>
</ul>
<h2 id="heading-disable-root-and-password-login">Disable root and password login</h2>
<p>I'll need to log in to my Vultr as <code>root</code> one last time:</p>
<pre><code class="lang-bash">ssh root@myvultrserver.com
</code></pre>
<p>Now I need to edit the <code>/etc/ssh/sshd_config</code> file to disable root and password login:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># root@vultr</span>

<span class="hljs-comment"># /etc/ssh/sshd_config</span>

<span class="hljs-comment"># change 'PermitRootLogin' from 'yes' to 'no'</span>
PermitRootLogin no
<span class="hljs-comment"># add 'PasswordAuthentication no' to bottom of file</span>
PasswordAuthentication no
</code></pre>
<p>Once the file is saved, I need to restart the <code>sshd</code> service on the server:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># root@vultr</span>

rcctl restart sshd <span class="hljs-comment"># press enter</span>

<span class="hljs-comment"># output</span>
sshd(ok)
sshd(ok)
</code></pre>
<p>Now I can log out of the instance and see if the changes took effect:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># root@vultr</span>
<span class="hljs-built_in">exit</span> <span class="hljs-comment"># press enter</span>

<span class="hljs-comment"># idev@MSI</span>
ssh root@myvultrserver.com <span class="hljs-comment"># press enter</span>

<span class="hljs-comment"># output</span>
root@sharif.gg: Permission denied (publickey,keyboard-interactive).
</code></pre>
<p>This is what I wanted to see -- an error message when trying to log in as <code>root</code> via <code>ssh</code>.</p>
<p>As a final test, I want to see if I can run an elevated command as my non-root user:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># idev@MSI</span>

ssh myvultrserver.com <span class="hljs-comment"># press enter</span>

<span class="hljs-comment"># login successful -- sharif@vultr</span>

<span class="hljs-comment"># attempt to edit a readonly file with sudo</span>
sudo vim /etc/ssh/sshd_config <span class="hljs-comment"># press enter</span>

<span class="hljs-comment"># prompted for password</span>
Password: <span class="hljs-comment"># enter password</span>

sharif is not <span class="hljs-keyword">in</span> the sudoers file. <span class="hljs-comment"># error</span>
This incident has been reported to the administrator.
</code></pre>
<p>I'm feeling pretty good about this, but there is a looming question...</p>
<h2 id="heading-what-if-i-need-root">What if I need root?</h2>
<p>There is a good chance I'll want to change something else on my server that requires <code>root</code> privileges.</p>
<p>But how do I do that if I just disabled root login and password login?</p>
<p>Thankfully, cloud services like Vultr offer a backdoor through their web console, where you can access a special web terminal with root access.</p>
<p>The web console is usually located in the dashboard/account management area for your cloud provider and is only available once you've logged into your account.</p>
]]></content:encoded></item><item><title><![CDATA[Managing Multiple SSH Keys on Both Ends]]></title><description><![CDATA[You might have multiple servers that you need to log into from one client machine.
Similarly, you might have multiple devices that need to all log into the same server.
You might even have multiple clients that all need to be able to log into multipl...]]></description><link>https://blog.saleshorse.org/managing-multiple-ssh-keys-on-both-ends</link><guid isPermaLink="true">https://blog.saleshorse.org/managing-multiple-ssh-keys-on-both-ends</guid><category><![CDATA[ssh]]></category><category><![CDATA[ssh-keys]]></category><category><![CDATA[Linux]]></category><category><![CDATA[linux for beginners]]></category><category><![CDATA[Security]]></category><dc:creator><![CDATA[Sharif Elkassed]]></dc:creator><pubDate>Mon, 15 May 2023 11:00:21 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/rNN8leuLnU4/upload/48224718b3e37e3f1968ce942b8798f6.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>You might have multiple servers that you need to log into from one client machine.</p>
<p>Similarly, you might have multiple devices that need to all log into the same server.</p>
<p>You might even have multiple clients that all need to be able to log into multiple servers, all using SSH with public key authentication -- meaning no password.</p>
<p>This post will guide you through all of the above scenarios, and explain the "need to know" bits of <code>ssh</code>, <code>ssh-keygen</code>, and <code>ssh-copy-id</code> .</p>
<p>If you're already familiar with generating and copying keys, skip to the <em>Multiple Keys on the Client</em> section.</p>
<h2 id="heading-prework">Prework</h2>
<p><em>Note: These instructions assume you are on MacOS, Linux, or WSL</em></p>
<h3 id="heading-make-sure-ssh-is-installed">Make sure <code>ssh</code> is installed</h3>
<p>First, execute the following command on <strong>both the client and the server</strong> to make sure <code>ssh</code> is installed:</p>
<pre><code class="lang-bash"><span class="hljs-built_in">which</span> ssh
</code></pre>
<p>The command above should output a file path.</p>
<p>If not, install <code>ssh</code>:</p>
<pre><code class="lang-bash">sudo apt install openssh-client
</code></pre>
<ul>
<li>after installing, you may need to restart your terminal for the system to recognize <code>ssh</code> as a command</li>
</ul>
<h3 id="heading-authorizedkeys-file"><code>authorized_keys</code> file</h3>
<p>On the <strong>server</strong>, we'll want to check for the <code>~/.ssh/authorized_keys</code> file. If it doesn't exist, don't worry -- it will be automatically created with the <code>ssh-copy-id</code> command in the following steps</p>
<p>The <code>authorized_keys</code> file contains a list of <strong>public keys</strong> (one per line). The end of each line shows the username and hostname of the machine from which the public key was generated.</p>
<p>If the file doesn't exist, it just means that you have not yet copied any keys to this server with the <code>ssh-copy-id</code> command.</p>
<h3 id="heading-ssh-folder"><code>.ssh</code> folder</h3>
<p>On the <strong>client</strong>, this folder lives in the <code>/home/&lt;yourUserName&gt;</code> directory.</p>
<p>If this folder doesn't exist, it will be created when you generate the key pair with <code>ssh-keygen</code>.</p>
<p>Any private/public key pairs you generate will live in this folder.</p>
<p>Private/public key pairs share the same name; the private key has <strong>no extension</strong>, and the public key has a <code>.pub</code> extension.</p>
<p>Again, if there are no keys here don't worry.</p>
<p>However, if there <em>are</em> keys here, <strong>you may wish to back them up, as there is the potential to overwrite them</strong> in the following steps if we're not careful.</p>
<p>Backing up existing keys might look something like this:</p>
<pre><code class="lang-bash">mkdir backup_ssh_keys <span class="hljs-comment"># make a backup folder</span>
cp ~/.ssh -r backup_ssh_keys <span class="hljs-comment"># copy the ~/.ssh directory to the backup folder</span>
ls -la backup_ssh_keys/.ssh <span class="hljs-comment"># confirm the keys were copied successfully</span>
</code></pre>
<h2 id="heading-ssh-keygen"><code>ssh-keygen</code></h2>
<p>Generating a key pair is done via the <code>ssh-keygen</code> command.</p>
<p>If you have <code>ssh</code> installed, then you have access to <code>ssh-keygen</code>.</p>
<p>The public/private key pairs are generated using an encryption algorithm. The specifics of the algorithm aren't pertinent to the scope of this blog post. However, the algorithm you supply to the <code>ssh-keygen</code> command could come in handy, depending on your situation.</p>
<p>(More on that later).</p>
<p>The important thing to understand is the relationship between the public and private keys:</p>
<p>I like to think of the public key as a lock on a door, and the private key is what unlocks it.</p>
<h3 id="heading-check-the-ssh-folder">Check the <code>~/.ssh</code> folder</h3>
<pre><code class="lang-bash">ls -la ~/.ssh
</code></pre>
<p>If there are no keys in this folder, you can move to the next section.</p>
<p>If there are keys here, take note of the names of the keys. You'll need to reference them in a moment.</p>
<h3 id="heading-generate-the-key">Generate the key</h3>
<p>If the <code>ssh-keygen</code> command is run without any parameters, it will use the <code>rsa</code> algorithm by default, and generate the keys <code>id_rsa</code> and <code>id_rsa.pub</code> in your <code>~/.ssh</code> folder.</p>
<p>The algorithm that is used to generate the key pair is also the default name of the key pair.</p>
<p>Using the default key name and location makes it easier to <code>ssh</code> into the server without any additional configuration.</p>
<p>This is why we took down the names of any keys that already existed in our <code>~/.ssh</code> folder -- if there is already a key named <code>id_rsa</code> in that folder, then we may want to consider using a different algorithm so that we can keep the default name for an easier configuration.</p>
<p>If you're set on using the same algorithm, don't worry -- we'll cover that too.</p>
<p>Generate the key using either <code>ssh-keygen</code> on its own for the defaults, or use <code>ssh-keygen -t &lt;algorithm&gt;</code> to use a specific algorithm (<code>man ssh-keygen</code> to see all algorithm options).</p>
<p>You are then prompted to either accept the default name and path for the file or supply a new one:</p>
<pre><code class="lang-bash">Generating public/private rsa key pair.
Enter file <span class="hljs-keyword">in</span> <span class="hljs-built_in">which</span> to save the key (/home/&lt;yourUsername&gt;/.ssh/id_rsa):
</code></pre>
<p>If you already have a key named <code>id_rsa</code> in this folder, you'll want to provide a new name for the key here. Otherwise, the old key will be overwritten, and you'll no longer be able to access that server resource until you restore from the backup we created or a new key pair is generated.</p>
<p>If there are no conflict concerns, go ahead and accept the default.</p>
<h2 id="heading-ssh-copy-id"><code>ssh-copy-id</code></h2>
<p>This command is used from the <strong>client side</strong> to copy the public key over to the <code>authorized_keys</code> file on the server. Just like <code>ssh-keygen</code>, you have access to this command as long as <code>openssh-client</code> is installed.</p>
<p>If you've only got one key pair in your <code>.ssh</code> folder, it's pretty easy.</p>
<p>If you've got more than one key pair, it's still pretty easy (<code>ssh-copy-id</code> uses the most recent key pair in your <code>.ssh</code> folder to determine which key to copy over)</p>
<ul>
<li><p>if it's the most recently-generated key you're wanting to copy over (and you used the default key name and location), you can leave the command as-is</p>
</li>
<li><p>if not, you can specify the path to the file using the <code>-i</code> flag</p>
</li>
</ul>
<p>Run the command with this syntax:</p>
<pre><code class="lang-bash">ssh-copy-id username@server
</code></pre>
<p>Again, if you're not wanting to use the most-recently-generated key pair (or need to specify the non-default key name you set), use the <code>-i</code> flag, specifying the file path of the public key like this:</p>
<pre><code class="lang-bash">ssh-copy-id -i /home/&lt;yourUsername&gt;/.ssh/&lt;nameOfKey&gt;.pub
</code></pre>
<p>If everything worked as expected, you will be prompted for the password for your user on the server, and get a message like this:</p>
<pre><code class="lang-bash">Number of key(s) added: 1

Now try logging into the machine, with:   <span class="hljs-string">"ssh 'user@server'"</span>
and check to make sure that only the key(s) you wanted were added.
</code></pre>
<h2 id="heading-ssh"><code>ssh</code></h2>
<p>To doubly confirm that everything worked correctly, let's log out of the server and <code>ssh</code> back into it to make sure we aren't prompted for a password.</p>
<pre><code class="lang-bash"><span class="hljs-built_in">exit</span> <span class="hljs-comment"># log out of the server</span>

<span class="hljs-comment"># now back on client machine</span>
ssh user@server
</code></pre>
<p>You should be logged right in, without being prompted for a password.</p>
<p>If you were prompted, log out of the server again and run the following to see what the issue is:</p>
<pre><code class="lang-bash">ssh user@server -v <span class="hljs-comment">#verbose output for debugging</span>
</code></pre>
<p>The most likely cause for this issue is that you need to specify the path to the key file because you supplied a custom filename to <code>ssh-keygen</code> instead of using the default. If that is the case, try running the command with the <code>-i</code> flag, and supply the path to the file:</p>
<pre><code class="lang-bash">ssh -i /home/.ssh/&lt;yourCustomKey&gt; user@server
</code></pre>
<p>If this works, but you don't want to have to type out the file path each time you access your server, we cover a few options in the next section.</p>
<h2 id="heading-multiple-keys-on-the-client">Multiple Keys on the Client</h2>
<p>You might need to access multiple servers from the same client machine. That means you need multiple keypairs on the client.</p>
<p>I prefer to be able to log into a server without having to remember the specific key (or path to the key) that I need to use. I want to be able to run <code>ssh user@server</code> every time, and have it work consistently.</p>
<p>For this to work, we have some options:</p>
<ul>
<li><p>if the algorithm used for the key isn't important, simply generate keys for other servers with different algorithms</p>
<ul>
<li>this works if you only need to log into a small number of servers, as there are a limited number of algorithms</li>
</ul>
</li>
<li><p>another option is to use a configuration file to specify which key to use for which server</p>
<ul>
<li>more flexible, but requires knowing how the configuration file works</li>
</ul>
</li>
</ul>
<h3 id="heading-example-with-different-algorithms">Example with Different Algorithms</h3>
<p>From my laptop, I need to access two servers:</p>
<ul>
<li><p>a remote server hosted on <a target="_blank" href="https://www.vultr.com/">vultr</a></p>
</li>
<li><p>a Raspberry Pi on my local network</p>
</li>
</ul>
<p><em>NOTE: for the vultr instance we are using</em> <code>root</code> <em>to keep the example simple. Follow instructions for your cloud provider on setting up SSH keys for a non-root user and then disable root login and password login.</em></p>
<h4 id="heading-using-the-ed25519-algorithm-for-vultr">Using the <code>ed25519</code> algorithm for vultr</h4>
<p>First I would generate the key pair for the vultr server using the <code>ed25519</code> algorithm:</p>
<pre><code class="lang-bash">ssh-keygen -t ed25519
</code></pre>
<p>I press <code>Enter</code> through all the prompts to accept the default file name and location, choosing not to set a passphrase.</p>
<p>Now I copy the key over to my vultr server with <code>ssh-copy-id</code></p>
<pre><code class="lang-bash">ssh-copy-id root@myvultrserver.com
</code></pre>
<p>I get the following output, and am then prompted for the <code>root</code> user's password:</p>
<pre><code class="lang-bash">/usr/bin/ssh-copy-id: INFO: Source of key(s) to be installed: <span class="hljs-string">"/home/idev/.ssh/id_ed25519.pub"</span>
/usr/bin/ssh-copy-id: INFO: attempting to <span class="hljs-built_in">log</span> <span class="hljs-keyword">in</span> with the new key(s), to filter out any that are already installed
/usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- <span class="hljs-keyword">if</span> you are prompted now it is to install the new keys
root@myvultrserver.com<span class="hljs-string">'s password:</span>
</code></pre>
<p>I enter my password for the server and get the following output:</p>
<pre><code class="lang-bash">Number of key(s) added: 1

Now try logging into the machine, with:   <span class="hljs-string">"ssh 'root@myvultrserver.com'"</span>
and check to make sure that only the key(s) you wanted were added.
</code></pre>
<p>Because the default file path was used when generating the key, no file path needs to be specified with the <code>ssh</code> command and I can execute it just like it instructs me to in the output above:</p>
<pre><code class="lang-bash">ssh root@myvultrserver.com
</code></pre>
<p>I am successfully logged in using public key authentication since I was not prompted for a password.</p>
<h4 id="heading-using-the-default-rsa-algorithm-for-the-raspberry-pi">Using the default <code>rsa</code> algorithm for the Raspberry Pi</h4>
<p>Now that I've squared away my ssh configuration to my vultr instance, I generate the key pair for the Raspberry Pi, using the default <code>rsa</code> algorithm</p>
<pre><code class="lang-bash">ssh-keygen
</code></pre>
<p>Again I accept all the defaults at the prompts.</p>
<p>Since <code>ssh-copy-id</code> uses the most recently generated default identity file, we can use it just like we did for vultr, just replacing the server with the raspberry pi instead:</p>
<pre><code class="lang-bash">ssh-copy-id sharif@raspberrypi.local
</code></pre>
<p>We get similar output as before; the only difference is the username and host is for the raspberry pi instead of the vultr instance:</p>
<pre><code class="lang-bash">/usr/bin/ssh-copy-id: INFO: Source of key(s) to be installed: <span class="hljs-string">"/home/idev/.ssh/id_rsa.pub"</span>
/usr/bin/ssh-copy-id: INFO: attempting to <span class="hljs-built_in">log</span> <span class="hljs-keyword">in</span> with the new key(s), to filter out any that are already installed
/usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- <span class="hljs-keyword">if</span> you are prompted now it is to install the new keys
sharif@raspberrypi.local<span class="hljs-string">'s password:</span>
</code></pre>
<p>I enter the password and get the message that the key was added, and am instructed to attempt the public key authentication:</p>
<pre><code class="lang-bash">Number of key(s) added: 1

Now try logging into the machine, with:   <span class="hljs-string">"ssh 'sharif@raspberrypi.local'"</span>
and check to make sure that only the key(s) you wanted were added.
</code></pre>
<p>I type the command as instructed in the output</p>
<pre><code class="lang-bash">ssh sharif@raspberrypi.local
</code></pre>
<p>I am not prompted for a password once again, meaning the public key authentication was successful.</p>
<h3 id="heading-example-with-configuration-file">Example with Configuration File</h3>
<p>Let's pretend that any keys generated must use the <code>ed25519</code> algorithm.</p>
<p>For this example, we'll use the same two servers, but clear out the configuration so we can start fresh:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># as root on vultr server</span>
rm ~/.ssh/authorized_keys

<span class="hljs-comment"># as sharif on raspberry pi</span>
rm ~/.ssh/authorized_keys

<span class="hljs-comment"># as idev on client machine</span>
rm ~/.ssh/rd_rsa
rm ~/.ssh/rd_rsa.pub
rm ~/.ssh/id_ed25519
rm ~/.ssh/id_ed25519.pub
</code></pre>
<p>Now we can begin generating the keys on the client machine.</p>
<p>Since we know ahead of time that</p>
<ul>
<li><p>we want to use the <code>ed25519</code> algorithm to generate both key pairs</p>
</li>
<li><p>the algorithm used to generate the key pair is the default name of the key pair</p>
</li>
</ul>
<p>... we decide that we should not accept the default key pair name when prompted (otherwise the second key would overwrite the first), and give each key pair a more distinguishable name.</p>
<h4 id="heading-configuration-for-vultr-server">Configuration for vultr server</h4>
<pre><code class="lang-bash"><span class="hljs-comment"># on client machine</span>
ssh-keygen -t ed25519
</code></pre>
<p>This time when we are prompted, instead of accepting the default, we provide a custom file name, but in the default folder:</p>
<pre><code class="lang-bash">Generating public/private ed25519 key pair.
Enter file <span class="hljs-keyword">in</span> <span class="hljs-built_in">which</span> to save the key (/home/idev/.ssh/id_ed25519): /home/idev/.ssh/vultr_ed25519
</code></pre>
<p>Now we try to copy the key over to the vultr instance, but we get an error</p>
<pre><code class="lang-bash">ssh-copy-id root@myvultrserver.com
/usr/bin/ssh-copy-id: ERROR: No identities found <span class="hljs-comment">#error</span>
</code></pre>
<p>This is because we supplied a custom file name, and by default, <code>ssh-copy-id</code> only knows to look for <code>id_rsa</code>, <code>id_ed25519</code>, etc.</p>
<p>To fix this, we use <code>-i</code> and supply our custom key file. Since this is a one-time command, using the <code>-i</code> flag is fine.</p>
<pre><code class="lang-bash">ssh-copy-id -i /home/idev/.ssh/vultr_ed25519 root@myvultrserver.com
</code></pre>
<p>Now we see our familiar message, and are asked for <code>root@myvultrserver.com</code>'s password, and are then instructed to attempt logging in via <code>ssh</code></p>
<pre><code class="lang-bash">Number of key(s) added: 1

Now try logging into the machine, with:   <span class="hljs-string">"ssh 'root@myvultrserver.com'"</span>
and check to make sure that only the key(s) you wanted were added.
</code></pre>
<p>So we try to execute that command:</p>
<pre><code class="lang-bash">ssh root@myvultrserver.com
</code></pre>
<p>But we're prompted for a password. This is because we need to either:</p>
<ul>
<li><p>supply the identity file with the <code>-i</code> flag again (remember that by default it only knows to look for the keys with the name of the algorithm)</p>
</li>
<li><p>or</p>
</li>
<li><p>use a configuration file</p>
</li>
</ul>
<p>Since we are striving for consistency and ease of use, we opt for the configuration file (so we don't have to supply the path of the key want to use every time we <code>ssh</code> into the server).</p>
<p>First we create the configuration file and secure the permissions:</p>
<pre><code class="lang-bash">touch ~/.ssh/config &amp;&amp; chmod 600 ~/.ssh/config
</code></pre>
<p>Now we can open <code>~/.ssh/config</code> in a text editor and write our first configuration:</p>
<pre><code class="lang-bash">Host myvultrserver.com
    HostName myvultrserver.com
    User root
    IdentityFile ~/.ssh/vultr_ed25519
</code></pre>
<p>Once we save the file, we can <code>ssh</code> into our server like this:</p>
<pre><code class="lang-bash">ssh myvultrserver.com
</code></pre>
<p>Since everything is in the configuration file, we just set the user and the file path once, and now we have a convenient way to access the server.</p>
<p>Now we can do the same thing for our Raspberry Pi connection.</p>
<h4 id="heading-configuration-for-raspberry-pi">Configuration for Raspberry Pi</h4>
<p>We follow the same process as we did for vultr, except we name the identity file something else this time.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># on client machine</span>
ssh-keygen -t ed25519
Generating public/private ed25519 key pair.
Enter file <span class="hljs-keyword">in</span> <span class="hljs-built_in">which</span> to save the key (/home/idev/.ssh/id_ed25519): /home/idev/.ssh/rpi_ed25519
</code></pre>
<p>Now we copy the key over to the Raspberry Pi.</p>
<pre><code class="lang-bash">ssh-copy-id -i /home/idev/.ssh/rpi_ed25519 sharif@raspberrypi.local
</code></pre>
<p>We get the expected output, along with one last password prompt</p>
<pre><code class="lang-bash">/usr/bin/ssh-copy-id: INFO: Source of key(s) to be installed: <span class="hljs-string">"/home/idev/.ssh/rpi_ed25519.pub"</span>
/usr/bin/ssh-copy-id: INFO: attempting to <span class="hljs-built_in">log</span> <span class="hljs-keyword">in</span> with the new key(s), to filter out any that are already installed
/usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- <span class="hljs-keyword">if</span> you are prompted now it is to install the new keys
sharif@raspberrypi.local<span class="hljs-string">'s password:

Number of key(s) added: 1

Now try logging into the machine, with:   "ssh '</span>sharif@raspberrypi.local<span class="hljs-string">'"
and check to make sure that only the key(s) you wanted were added.</span>
</code></pre>
<p>Now we can add to the configuration file:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># ~/.ssh/config</span>

Host myvultrserver.com
    HostName myvultrserver.com
    User root
    IdentityFile ~/.ssh/vultr_ed25519

Host raspberrypi.local
    HostName raspberrypi.local
    User sharif
    IdentityFile ~/.ssh/rpi_ed25519
</code></pre>
<p>Finally, we access the Raspberry Pi with</p>
<pre><code class="lang-bash">ssh raspberrypi.local
</code></pre>
<h2 id="heading-accessing-one-server-from-multiple-clients">Accessing one server from multiple clients</h2>
<p>We've already done most of the heavy lifting, and at this point, we just need to connect the dots.</p>
<p>Connecting to multiple servers from one client (which we did in the last section) is more involved than accessing one server from multiple clients since we pretty much just do the same thing on each client.</p>
<p>In fact, if we use the same names for the keys when they are generated on the new client, we can just copy the existing configuration file on each new client.</p>
<p>Let's go through an example.</p>
<h3 id="heading-example">Example</h3>
<p>I just got a new laptop, and I want to be able to access my vultr instance from it.</p>
<p>In the previous examples, the file paths looked like <code>/home/idev/.ssh/...</code>since <code>idev</code> was the name of the user on that computer.</p>
<p>Just for funsies, we'll use <code>ihop</code> as the user on this laptop.</p>
<p>Once I have <code>openssh-client</code> installed on the new laptop, I generate a key, and name it the same thing as I did for the other laptop</p>
<pre><code class="lang-bash"><span class="hljs-comment"># on new laptop</span>
ssh-keygen -t ed25519

<span class="hljs-comment"># output</span>
Generating public/private ed25519 key pair.
Enter file <span class="hljs-keyword">in</span> <span class="hljs-built_in">which</span> to save the key (/home/ihop/.ssh/id_ed25519): /home/ihop/.ssh/vultr_ed25519 <span class="hljs-comment"># same key name, but on a different machine</span>
</code></pre>
<p>Now our normal song and dance of copying the key over</p>
<pre><code class="lang-bash">ssh-copy-id -i /home/ihop/.ssh/vultr_ed25519 root@myvultrserver.com
</code></pre>
<p>Then we make our new config file and adjust the permissions</p>
<pre><code class="lang-bash">touch ~/.ssh/config &amp;&amp; chmod 600 ~/.ssh/config
</code></pre>
<p>And now we can simply copy the same configuration from our other laptop, since the configuration is using relative paths, and does not specify a username</p>
<pre><code class="lang-bash"><span class="hljs-comment"># ~/.ssh/config</span>

Host myvultrserver.com
    HostName myvultrserver.com
    User root
    IdentityFile ~/.ssh/vultr_ed25519
</code></pre>
<p>Now we can use the same command (<code>ssh myvultrserver.com</code>) on both laptops and it works the same way.</p>
<p>This pattern can be followed for each new client machine that needs to access the server.</p>
<h2 id="heading-access-multiple-servers-from-multiple-clients">Access multiple servers from multiple clients</h2>
<p>Similar to the last section, this sounds more complicated than it is. You may have already figured it out if you've read this far.</p>
<p>In this last section, we accessed the same server from a new client machine by generating a new key with the same name, then copying the existing configuration to the new client machine.</p>
<p>As it stands, we have two clients accessing the vultr server, but what if we also wanted the new laptop to have access to the Raspberry Pi?</p>
<h3 id="heading-example-1">Example</h3>
<p>We follow the same steps as the last section, except we use the name of the first Raspberry Pi key we created instead of the vultr key:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># on new laptop</span>
ssh-keygen -t ed25519
Generating public/private ed25519 key pair.
Enter file <span class="hljs-keyword">in</span> <span class="hljs-built_in">which</span> to save the key (/home/ihop/.ssh/id_ed25519): /home/ihop/.ssh/rpi_ed25519
</code></pre>
<p>And copy the key</p>
<pre><code class="lang-bash">ssh-copy-id -i /home/ihop/.ssh/rpi_ed25519 sharif@raspberrypi.local
</code></pre>
<p>And update our configuration file (it's now identical to the one on the <code>idev</code> laptop):</p>
<pre><code class="lang-bash"><span class="hljs-comment"># ~/.ssh/config</span>

Host myvultrserver.com
    HostName myvultrserver.com
    User root
    IdentityFile ~/.ssh/vultr_ed25519

Host raspberrypi.local
    HostName raspberrypi.local
    User sharif
    IdentityFile ~/.ssh/rpi_ed25519
</code></pre>
<p>Apply this same pattern for as many clients and servers as you need.</p>
]]></content:encoded></item><item><title><![CDATA[Raspberry Pi Crash Course]]></title><description><![CDATA[My partner got me a Raspberry Pi (CanaKit) a few years back, and I only recently took the time to get to know how to use it.
I still barely know what I'm doing, but I know enough now to fix the initial issues I was having, so now I can move on to doi...]]></description><link>https://blog.saleshorse.org/raspberry-pi-crash-course</link><guid isPermaLink="true">https://blog.saleshorse.org/raspberry-pi-crash-course</guid><category><![CDATA[Raspberry Pi]]></category><category><![CDATA[Linux]]></category><category><![CDATA[Node.js]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[hardware]]></category><dc:creator><![CDATA[Sharif Elkassed]]></dc:creator><pubDate>Fri, 21 Apr 2023 11:06:45 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/Il6Sb0_fxuk/upload/d06ef5b532cebeaafc1eccd69685569e.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>My partner got me a Raspberry Pi (CanaKit) a few years back, and I only recently took the time to get to know how to use it.</p>
<p>I still barely know what I'm doing, but I know enough now to fix the initial issues I was having, so now I can move on to doing cool shit with it.</p>
<p>We're going to go over each of the issues I had, how I solved them, and finally -- make an LED blink using JavaScript.</p>
<h2 id="heading-issues-out-of-the-box">Issues Out of the Box</h2>
<p>My unit wasn't defective or anything. These were software issues. Still, I wasn't sure how to solve them; my normal approaches weren't working.</p>
<p>The first issue was that Bluetooth wouldn't work. Not that big of a deal, since I had a wired keyboard and mouse laying around. Still kind of annoying, but I was willing to make peace with it since it's a barebones computer.</p>
<p>The next issue was a bit more problematic, though. I was getting errors any time I tried to use <code>apt</code> or <code>apt-get</code>. Everything I came across online suggested I update the distro mirror URL in the <code>/etc/apt/sources.list</code> file.</p>
<p>I tried for a while to make this work, but it just wasn't happening. After I changed up my Google search a bit, I discovered that I was running an older distro, Stretch. All of the forums were referencing newer distros -- Buster and Bullseye.</p>
<p>This gave me a bit more hope. Upgrading the distro would surely fix my issues.</p>
<h2 id="heading-distro-updates">Distro Updates</h2>
<p>From a forum answer, I gleaned that Buster came after Stretch, and Bullseye came after Buster. It was recommended to first upgrade to Buster, then to Bullseye, rather than upgrading from Stretch straight to Bullseye.</p>
<p>Again, the forums suggested editing the <code>/etc/apt/sources.list</code> file, but this time instead of adding a new mirror URL, the suggestion was to change any instances of the word 'stretch' to 'buster', then run a <code>sudo apt-get update</code>.</p>
<p>This worked, but the next time I booted up the pi, I got an error saying that the root account was locked. I couldn't even access the desktop.</p>
<p>Googling some more, I discovered the Raspberry Pi Imager tool, which is what I should have been using from the start. At this point, I was glad I had the Cana Kit, because it came with a microSD caddy. Now all that was left to do to do was:</p>
<ul>
<li><p>put the microSD card from the pi into the caddy</p>
</li>
<li><p>plug the caddy into my laptop</p>
</li>
<li><p>open the imager tool</p>
</li>
<li><p>select the Bullseye distro</p>
</li>
<li><p>select the USB caddy</p>
</li>
</ul>
<p>From here, the imager tool took care of the rest. I popped the microSD back into the pi, and Bullseye loaded right up.</p>
<p>Once Bullseye was installed, I was able to connect to Bluetooth, and all of my shell commands worked the way I expected them to. With that, I proceeded to install <code>zsh</code>, <code>oh-my-zsh</code>, and <code>neovim</code>.</p>
<h2 id="heading-hardware-time">Hardware Time</h2>
<p>Now that my software issues were out of the way, it was finally time to work on the hardware. Well, almost.</p>
<p>The literature that came with my CanaKit had examples of setting up a circuit and then running it on the Pi using Python. I've used Python before and was confident I could get it to work in Python, but I am a big fan of sticking to one programming language for as many things as possible.</p>
<p>Seeing that JavaScript is ubiquitous these days, I figured there had to be some resources out there to guide me in that direction.</p>
<p>Sure enough, I was in luck. The first steps were to install Node.js and npm. That was simple enough, but I wasn't sure how Node could let me control the hardware. Then I realized, "Well shit, I don't actually know much about this hardware at all".</p>
<p>That was when I found <a target="_blank" href="https://medium.com/sysf/an-introduction-to-raspberry-pi-4-gpio-and-controlling-it-with-node-js-10f2ce41af12">this blog post</a>, which gave me a primer on how the pins on the pi work, the reasoning behind using resistors, and a few other electrical engineering tidbits.</p>
<p>The blog post also filled in my knowledge gap around how Node can interface with the Raspberry Pi -- through the <code>onoff</code> package. <code>onoff</code> implements the <a target="_blank" href="https://projects.raspberrypi.org/en/projects/physical-computing/1">GPIO logic</a> of the pins in Node.js, so that people like me who find that sort of thing intimidating can work with a simple/intuitive API and have fun with their pi (hey, that rhymes!)</p>
<p>Although the blog post had an example with an LED using pin 4, I didn't want to copy it directly. I always like to change things up a bit to make sure I understand the concepts, instead of just following someone else's work to the tee.</p>
<p>So what I decided to do was use the Python example from the CanaKit (which used pin 18), and convert the Python code they provided into Node.js code. Once the circuit was set up and the code was written, running it was as easy as <code>node led.js</code>.</p>
<p>I was pleasantly surprised at how straightforward the coding part of this was. I had expected to install a bunch of things to get it working.</p>
<h2 id="heading-extras">Extras</h2>
<p>At this point, I was feeling pretty good. I felt like I finally had a basic understanding of everything that came in my CanaKit, except for one box that said "breakout board" on it.</p>
<p>Off to the Google Machine again. Turns out, a breakout board is just a labeled (and therefore more convenient) set of pins for the Raspberry Pi.</p>
<p>When I was going through the LED example in the previous section, I had to keep referencing a GPIO chart that tells you which pin is which. The breakout board has labels next to each of the 40 pins so that you don't need to reference a chart.  </p>
<p>The tradeoff here is the form factor. The original 40 pins fit right in the tiny Raspberry Pi case. The breakout board attaches to the original 40 pins and sits outside the case. If the pins inside the Raspberry Pi were labeled, then the device would have to be made bigger to account for that.</p>
<h2 id="heading-next-steps">Next Steps</h2>
<p>This has got me excited about the possibilities of tinkering with hardware. I want to see how far I can take it, both on the hardware and the software side. How complex of a circuit can I make? What sort of useful hardware can I wire this up to? Can I have that hardware signal some software I write, based on the conditions of the circuit?</p>
<p>I hope you'll stick around to find out!</p>
]]></content:encoded></item><item><title><![CDATA[Getting a Salesforce access token with Postman]]></title><description><![CDATA[Why do I need an access token?
Access tokens are part of the authentication flow for accessing a resource that lives within Salesforce. Think of it as your way of proving that you are allowed to have access to that resource.  
For example, let's say ...]]></description><link>https://blog.saleshorse.org/salesforce-access-token-postman</link><guid isPermaLink="true">https://blog.saleshorse.org/salesforce-access-token-postman</guid><category><![CDATA[Salesforce]]></category><category><![CDATA[Postman]]></category><category><![CDATA[access-token]]></category><category><![CDATA[oauth]]></category><category><![CDATA[OAuth2]]></category><dc:creator><![CDATA[Sharif Elkassed]]></dc:creator><pubDate>Sun, 26 Mar 2023 05:03:27 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1677558266929/6471cefa-5516-473b-9c11-52a77a6dfcb8.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-why-do-i-need-an-access-token">Why do I need an access token?</h2>
<p>Access tokens are part of the authentication flow for accessing a resource that lives within Salesforce. Think of it as your way of proving that you are allowed to have access to that resource.  </p>
<p>For example, let's say we have a <code>@RestResource</code> REST API endpoint that we have created using Apex. We want to be able to consume the data from that REST API from an external web application.</p>
<p>The web application has to prove that it has access to the Salesforce environment to access the data behind the REST API. This is where access tokens come in.</p>
<p>Many different authorization flows in Salesforce use access tokens. This walkthrough will focus on the username/password auth flow. <strong>This is not a viable workflow for a production environment</strong>. However, it is a good workflow for testing out HTTP requests quickly.  </p>
<h2 id="heading-postman">Postman</h2>
<p>Postman is a piece of software that provides a user interface for retrieving access tokens and making HTTP requests.</p>
<p>In this walkthrough, we make use of the Salesforce Platform APIs Collection within Postman, which makes getting an access token relatively straightforward.</p>
<p>We can think of a Postman Collection as a group of pre-configured HTTP requests (<code>GET</code>, <code>POST</code>, etc.)</p>
<p>As such, the Salesforce Platform APIs Collection has many pre-configured requests. However, we are mainly concerned with the access token request.</p>
<p>The steps below will illustrate:</p>
<ul>
<li><p>Adding the Salesforce Platform APIs Collection to Postman</p>
</li>
<li><p>Making the access token request</p>
</li>
<li><p>Using the access token</p>
</li>
<li><p>Troubleshooting for several scenarios</p>
</li>
</ul>
<h2 id="heading-1-postman-log-in-sign-up">1. Postman Log in / Sign Up</h2>
<ul>
<li><p>Head over to <a target="_blank" href="https://postman.com">postman.com</a> (or use the desktop app)</p>
</li>
<li><p>Log in to your Postman account (or create a new one)</p>
</li>
</ul>
<h2 id="heading-2-add-and-fork-the-collection">2. Add and Fork the Collection</h2>
<ul>
<li><p>At the top of the page, click the search box</p>
</li>
<li><p>Search <code>Salesforce Platform APIs</code></p>
</li>
</ul>
<p><img src="https://i.imgur.com/J9NyIV4.png" alt="Salesforce Platform APIs Postman Collection" /></p>
<ul>
<li><p>Click on the collection</p>
</li>
<li><p>Press the 'Fork' button to fork the collection</p>
</li>
</ul>
<p><img src="https://i.imgur.com/cct9sDo.png" alt="Postman Collection Fork Button" /></p>
<ul>
<li><p>Give the fork a name</p>
</li>
<li><p>Select a Workspace for the fork</p>
</li>
<li><p>Press the 'Fork Collection' button</p>
</li>
</ul>
<p>The collection should now be open in your workspace.</p>
<h2 id="heading-3-request-access-token">3. Request Access Token</h2>
<p>Your forked collection should have opened to the 'Authorization' tab.</p>
<p><img src="https://i.imgur.com/atEb7VY.png" alt="Forked Collection Authorization Tab" /></p>
<ul>
<li><p>Scroll down to the bottom of the Authorization tab</p>
</li>
<li><p>Leave all default values</p>
</li>
<li><p>Press the 'Get New Access Token' button</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1679806707029/330c9b2a-1e82-4642-9129-e2498de86091.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>The Salesforce login page will appear in a new window</p>
<ul>
<li>If you are not presented with a standard username/password prompt, see the <strong>Troubleshooting</strong> section below</li>
</ul>
</li>
<li><p>Enter your Salesforce username and password</p>
</li>
<li><p>Log in</p>
</li>
<li><p>You should receive the following prompt</p>
</li>
</ul>
<p><img src="https://i.imgur.com/uqPfVpi.png" alt="Salesforce Allow Access Modal" class="image--center mx-auto" /></p>
<ul>
<li><p>Click 'Allow'</p>
</li>
<li><p>You will be redirected back to Postman, and should see the following screen</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1679749493045/877d14d0-29b3-44de-89ed-c56ebcac4c70.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Copy the access token to your clipboard so that we can use it in the next step</p>
</li>
<li><p>Click the 'Use Token' button</p>
</li>
</ul>
<h2 id="heading-4-use-the-access-token">4. Use the Access Token</h2>
<p>Now that we have the token, we can use it in a request.</p>
<h4 id="heading-construct-request-url">Construct request URL</h4>
<ol>
<li><p>Get your base URL for the request</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1679803388069/72154166-2cd2-4ade-af6a-26311c27a952.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Add <code>https://</code> to the beginning of the base URL</p>
</li>
<li><p>Add <code>/&lt;your_api_endpoint_route&gt;</code> to the end of the base URL</p>
</li>
</ol>
<p>For our example, we are using a <code>@RestResource</code> that lives at <code>/services/apexrest/Contacts</code></p>
<p>Therefore, the full request URL we end up with is:  </p>
<pre><code class="lang-http"><span class="hljs-attribute">https://saleshorse-dev-ed.develop.my.salesforce.com/services/apexrest/Contacts</span>
</code></pre>
<h4 id="heading-create-a-postman-collection">Create a Postman Collection</h4>
<p>Chances are, you're going to want to make this same request (or a very similar one) at a later date. And since this request is separate from the access token request, we can store it in a separate Collection.</p>
<ol>
<li><p>Click the 'Collections' tab in Postman, then click the '+' button</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1679804459959/42a2381f-b10e-432a-820f-ac7405488e51.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Click the pencil icon to rename the Collection</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1679804651015/38edd11e-ed3d-4303-b6af-279480b4a441.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Select 'OAuth 2.0' under the 'Type' dropdown</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1679804723542/6356c210-e662-4b9f-82bb-1d6b5e1cb9fc.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Scroll down and paste the access token into the 'Token' field, being sure there is no whitespace</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1679804865014/260c8744-6c48-4b6d-a907-491b3638ef95.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Add a request to the collection by expanding the collection in the sidebar and clicking the 'Add a request' link (or by pressing the three dots (...) and selecting 'Add request')</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1679805507160/49fb54ed-ec0a-463a-a27d-53985d612651.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Rename the request from the default 'New Request'</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1679805637369/aca540a5-acf0-4de1-9b01-dcd77ab6d3bd.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Select the 'Authorization' tab, and ensure that 'Inherit auth from parent' is selected as the 'Type'. (Because we entered the token for the entire collection, we don't have to enter it again for each request within that collection)</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1679806061279/819651d0-f659-48fc-ae55-7016c0ea5b42.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Paste in the request URL and press 'Send'</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1679806166869/d9bf0873-fc91-489d-ad2d-e3d91c81bfbc.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>The request result appears at the bottom of the window</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1679806260469/e47fd902-e62e-4900-93f0-6276ef14e935.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Save the Collection</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1679804994302/5c32ac79-ff4d-4601-bc23-6b316ada316a.png" alt class="image--center mx-auto" /></p>
</li>
</ol>
<h2 id="heading-troubleshooting">Troubleshooting</h2>
<h3 id="heading-trouble-getting-access-token">Trouble getting access token</h3>
<p>If a list of saved usernames appears, click the 'Log in with a Different Username' link.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1679719708982/9e91e407-9538-4ef5-977b-268e97009d3e.png" alt class="image--center mx-auto" /></p>
<ul>
<li><p>The Salesforce login page will appear in a new window</p>
<ul>
<li><p>If the URL in the new window starts with <code>login.salesforce.com</code></p>
<ul>
<li><p>Click the 'Use Custom Domain' link</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1679720099760/dd5b237e-4e93-45dc-8e11-deb643e20396.png" alt class="image--center mx-auto" /></p>
</li>
</ul>
</li>
<li><p>If the URL in the new window is the URL of your target Salesforce org, skip this step and enter your username and password (the 'Use Custom Domain' link will not be present)</p>
</li>
</ul>
</li>
</ul>
<h4 id="heading-find-your-domain-url">Find your domain URL</h4>
<ul>
<li><p>Log into the Salesforce org that you are requesting an access token for</p>
</li>
<li><p>Click on your avatar in the top-right</p>
</li>
<li><p>Copy the URL up until the <code>.my.salesforce.com</code></p>
<p>  <a target="_blank" href="https://imgur.com/5EMQWf9"><img src="https://i.imgur.com/5EMQWf9.png" alt class="image--center mx-auto" /></a></p>
</li>
<li><p>Paste it into the 'Custom Domain' field (Salesforce automatically adds the <code>.my.salesforce.com</code></p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1679720598230/4dd183e9-174e-4525-989c-9d9d6e1b95cd.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Press 'Continue'</p>
</li>
<li><p>Log in with your username and password</p>
</li>
</ul>
<h3 id="heading-trouble-using-access-token">Trouble using access token</h3>
<p>If you receive the following error when using the access token in a request, follow the steps below to resolve the issue:</p>
<pre><code class="lang-json">{
    <span class="hljs-attr">"message"</span>: <span class="hljs-string">"Session expired or invalid"</span>,
    <span class="hljs-attr">"errorCode"</span>: <span class="hljs-string">"INVALID_SESSION_ID"</span>
  }
</code></pre>
<h4 id="heading-request-another-token">Request another token</h4>
<p>Access tokens are only valid for a limited time, so you may just be seeing this message because you need to request a fresh token to use in your request.</p>
<h4 id="heading-check-that-the-token-was-copied-and-pasted-correctly">Check that the token was copied and pasted correctly</h4>
<p>You may have missed a character when highlighting the access token. Request a new token, and make sure that is not the issue.</p>
<p>If you are sure you have copied the token correctly, check that there are no line breaks or leading/trailing spaces in the access token.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1679750240629/e398fc61-7399-4189-96fb-66879e7420c3.png" alt class="image--center mx-auto" /></p>
<p>In the screenshot above, Postman is trying to hint to us that we have a line break at the end of our token, by displaying a carriage return symbol (highlighted in red).</p>
<p>To fix this, place your cursor on the empty line below the token, then press the backspace key. You should see the token input shrink to fit the length of your token, and the carriage return symbol should go away:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1679750505927/23592120-bdc1-43cb-b20b-43d3f53ca43c.png" alt class="image--center mx-auto" /></p>
<h4 id="heading-make-sure-the-token-was-requested-for-the-correct-salesforce-org">Make sure the token was requested for the correct Salesforce org</h4>
<p><strong>TL;DR -- log out of any logged-in Salesforce sessions, and request the token again</strong></p>
<p>If you frequently log into more than one Salesforce org, you may run into this issue.  </p>
<p>When you log into a Salesforce org, it creates a <em>session</em> within your browser. This is why you can click a link for a record in that Salesforce org and be taken straight to the record, without having to log in.  </p>
<p>If you are using the Postman web application in the same browser session as the Salesforce org you are logged into, Postman is going to request the access token for the org you're already logged into.</p>
<p><strong>If the org you're already logged into isn't the org you're requesting the access token for, this isn't the behavior you want.</strong></p>
<p>If you're still scratching your head, let's work through an example:</p>
<p>Say your company uses Salesforce internally for things like showing company news and events. You logged into the internal Salesforce org when you started your day, looked over the announcements, then started working on integrating a REST API endpoint for a client, which lives in the <em>client's</em> Salesforce org.  </p>
<p>When you request the access token with Postman through the <strong>Postman web app in the browser</strong>, the steps go something like this:</p>
<ol>
<li><p>Postman requests an access token from the Salesforce access token endpoint. (Note that this endpoint is not specific to any one org)</p>
</li>
<li><p>Salesforce says "I might have a token for you, but first I need you to tell me who you are", and attempts to relay this message back to Postman through the browser</p>
</li>
<li><p>But before that "who are you?" message can even get back to Postman your browser session butts in and says, "No need! Check it out, they're already logged in."</p>
</li>
<li><p>Salesforce says "Whatever. Here's your token."</p>
</li>
<li><p>You are then issued a token for the incorrect org, because you never got the chance to specify which org you wanted the token for</p>
</li>
</ol>
<p>To get around this issue, you can either:</p>
<ol>
<li><p>Log out of the Salesforce org in your current browser session</p>
</li>
<li><p>Go to Postman in a private/incognito browser window to force a new session</p>
</li>
</ol>
<p>After picking one of the two options, request a new token via Postman, and enter the credentials for the desired org when prompted.</p>
]]></content:encoded></item></channel></rss>