<?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" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[John Switzerland's blog]]></title><description><![CDATA[Thoughts, notes and references]]></description><link>https://blog.johnswitzerland.com/</link><image><url>https://blog.johnswitzerland.com/favicon.png</url><title>John Switzerland&apos;s blog</title><link>https://blog.johnswitzerland.com/</link></image><generator>Ghost 5.26</generator><lastBuildDate>Thu, 23 Apr 2026 09:03:47 GMT</lastBuildDate><atom:link href="https://blog.johnswitzerland.com/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[Dynamic Update for OCI's Ingress Firewall Rules]]></title><description><![CDATA[Using Oracle's OCI API to update firewall ingress rules dynamically, to allow for dynamic IP address lockdown. ]]></description><link>https://blog.johnswitzerland.com/updating-oci-firewall-dynamically/</link><guid isPermaLink="false">641612cc77eb7300016f0284</guid><category><![CDATA[free-tier]]></category><category><![CDATA[oracle]]></category><category><![CDATA[tailscale]]></category><category><![CDATA[security]]></category><category><![CDATA[API]]></category><category><![CDATA[OCI]]></category><dc:creator><![CDATA[John Switzerland]]></dc:creator><pubDate>Sun, 19 Mar 2023 15:24:02 GMT</pubDate><media:content url="https://blog.johnswitzerland.com/content/images/2023/03/king-s-church-international-3mjspmQDM_M-unsplash.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://blog.johnswitzerland.com/content/images/2023/03/king-s-church-international-3mjspmQDM_M-unsplash.jpg" alt="Dynamic Update for OCI&apos;s Ingress Firewall Rules"><p><strong>Problem</strong>: I want to only open access to some services on my OCI Instance(s) to my home IP address (pihole for example). However, my home IP is dynamic, changing every week or so or whenever my router restarts.</p><p><strong>Solution</strong>: Have my router do a HTTP GET on a publicly open website running on an instance, which allows the site to record my home public IP. A script runs periodically on an instance, and if the IP has changed, update OCI ingress firewall rules (using the OCI Python API) to allow that IP address.</p><h3 id="warnings">Warnings:</h3><ol><li>Backup your rules first. Since the API doesn&apos;t let us modify rules in-place, we need to replace them all with updated versions - so if something goes wrong, you could end up with an empty ingress rule-set, and be locked out.</li><li>Have a backup way into your instances in case something like the above happens - see the <a href="https://blog.johnswitzerland.com/oracle-free-tier-tailscale/">Oracle Free Tier VPS - Best Practices</a> post (e.g. use the magical Tailscale, and/or have a local console login ready and tested).</li><li>Don&apos;t publicise the URL where your incoming PHP script lives (or the key) from step 2 below. This should really be more secure (https, a key exchange) to avoid risk of abuse / denial of service attacks - left as an exercise for the reader :-)</li><li><strong>Caveat Emptor</strong>. Should be obvious, but use anything here at your own risk. This works for me, and has done so flawlessly for some time, but the script was thrown together quickly, APIs change - YMMV. </li></ol><h2 id="step-1router-ping">Step 1 - Router Ping</h2><p>I have a MikroTik router which makes this nice and easy. I initially wrote a RouterOS script to only ping my site when the router&apos;s assigned public IP address changed, but shortly after a reboot / IP change Internet access was unreliable so pings could go missing - so the script now pings the website every minute.</p><pre><code class="language-routeros">{
:local posterURL &quot;http://aa.bb.cc.dd/poster.php?key=12345&quot;;

# GET to webserver watching for calls, so it gets current public IP
/tool fetch url=$posterURL keep-result=no;
}
</code></pre><p></p><h2 id="step-2script-to-record-public-ip">Step 2 - Script to Record Public IP</h2><p>Save the IP address of the client to a local file using a simple PHP script served by a webserver on an instance (Apache, Nginx, your choice). Simple double-check for a key so nobody else can potentially DoS us.</p><pre><code class="language-PHP">&lt;?php
        $ipaddress = getenv(&quot;REMOTE_ADDR&quot;) ;
        echo &quot;Client IP $ipaddress &lt;br&gt;&quot;;

        $queryString = $_SERVER[&apos;QUERY_STRING&apos;];

        if (str_contains($queryString, &apos;key=12345&apos;)) {
                $myfile = fopen(&quot;/abc/xyz/currentip.txt&quot;, &quot;w&quot;) or die(&quot;Unable to open file. Argh.&quot;);
                fwrite($myfile, $ipaddress);
                fclose($myfile);

                echo &quot;Done.&lt;br&gt;&quot;;
        } else {
                echo &quot;Undone.&lt;br&gt;&quot;;
        }
?&gt;</code></pre><p></p><h2 id="step-3use-oci-api-to-update-firewall-rules">Step 3 - Use OCI API to Update Firewall Rules</h2><p>Import libraries we need - the oci one is key, you&apos;ll need to install it first, see the quickstart reference docs link below. Also setup logging - level is debug since we want lots of detail - and the output format I prefer:</p><pre><code class="language-python">import oci
import json
import os
from datetime import datetime
import logging
import os.path
from os import path

logging.basicConfig(level=logging.DEBUG, format=&apos;%(asctime)s - %(name)s - %(levelname)s - %(message)s&apos;)


</code></pre><p></p><p>Check if we have an IP address saved from home router (by above PHP script). If one exists and it is different from the last address we used for an update, then proceed. Otherwise exit now.</p><pre><code class="language-python"># first check for IP posted from home router, if none then exit.
ipFilename = &apos;/xxx/xxx/currentip.txt&apos;

savedIP = &apos;10.0.0.123&apos;  # safe fallback JIC
try:
  with open(ipFilename) as file:
    savedIP = file.read().strip()
    logging.info(f&apos;Got IP: {savedIP}&apos;)
except:
  logging.info(&apos;No IP file, exiting peacefully.&apos;)
  exit()

lastIPFilename = &apos;/tmp/poster-lastip.txt&apos;

# have a posted home IP, check if different from last seen/update
if path.exists(lastIPFilename):
  # contains last IP we updated the FW rules to, so if it&apos;s the same as the one we just got, we can ignore and no update needed.
  with open(lastIPFilename) as lastIPfile:
    lastIP = lastIPfile.read().strip()
    logging.info(f&apos;Got last update IP: {lastIP}&apos;)
    if (lastIP == savedIP):
      # is same as last update, so no need to do another update
      os.remove(ipFilename)
      logging.info(&apos;Same IP as last update, exiting.&apos;)
      exit()
    else:
      logging.info(f&apos;New IP {savedIP} differs from last update IP {lastIP}, carrying on.&apos;)
else:
  logging.info(f&apos;No last update IP saved, so carrying on.&apos;)
</code></pre><p></p><p>Init the main oci object, reading config from the OCI config file usually saved locally in ~/.oci along with your private key (xxx.pem), and set values we&apos;ll need later:</p><pre><code class="language-python">config = oci.config.from_file()

# Initialize service client with default config file
core_client = oci.core.VirtualNetworkClient(config)

# consts.
network_compartment_name = &quot;ManagedCompartmentForPaaS&quot;
vcn_cidr_range = &quot;10.0.0.0/16&quot;
vcn_subnet_cidr_range = &quot;10.0.0.0/24&quot;

# hardcoded specific values - can get most programatically, but easier to hardcode for now.
vcn_id = &quot;ocid1.vcn.oc1.XXXXXX-YYYYYYYYyjkpeva&quot;
vcn_name = &quot;vcn-XXXX-YYYY&quot;
# hardcode security list name
sl_name = &quot;Default Security List for vcn-XXXX-YYYY&quot;
vcn_sl_id = &quot;ocid1.securitylist.oc1.AAAAAA-BBBBBBBBBBmegoq&quot;

</code></pre><p></p><p>Get existing ingress rules (and dump them to disk just in case before we modify them). The API doesn&apos;t allow us to modify existing individual rules, instead we need to <strong>replace them all </strong>with our modified versions:</p><pre><code class="language-python"># get the current security list data (ingress rules since we&apos;ll be updating those)
get_security_list_response = core_client.get_security_list(
    security_list_id=vcn_sl_id)
current_vcn_sl=json.loads(str(get_security_list_response.data.ingress_security_rules))

# write details before modifying... JIC
with open(&apos;vcn_ingress_list_before.json&apos;, &quot;w&quot;) as f:
   json.dump(current_vcn_sl, f, ensure_ascii=False, indent=4)
</code></pre><p></p><p>Define the rules we wish to update, making any required changes - in this case, we only change the IP address. In this example, I&apos;m updating 2 rules for DNS via TCP and UDP respectively. To see how existing rules are represented, inspect the populated &quot;<em>current_vcn_sl</em>&quot; variable above which will contain all your existing rules and their config parameters: </p><pre><code class="language-python"># updated rules that we want to replace/update existing ones
sl_update_rules=[
    {
      &quot;description&quot;: &quot;DNS TCP&quot;,
      &quot;icmp_options&quot;: None,
      &quot;is_stateless&quot;: False,
      &quot;protocol&quot;: &quot;6&quot;,
      &quot;source&quot;: &apos;&apos; + savedIP + &apos;/32&apos;,
      &quot;source_type&quot;: &quot;CIDR_BLOCK&quot;,
      &quot;tcp_options&quot;: {
        &quot;destination_port_range&quot;: {
          &quot;max&quot;: 53,
          &quot;min&quot;: 53
        },
        &quot;source_port_range&quot;: None
      },
      &quot;udp_options&quot;: None
    },

    {
      &quot;description&quot;: &quot;DNS UDP&quot;,
      &quot;icmp_options&quot;: None,
      &quot;is_stateless&quot;: False,
      &quot;protocol&quot;: &quot;17&quot;,
      &quot;source&quot;: &apos;&apos; + savedIP + &apos;/32&apos;,
      &quot;source_type&quot;: &quot;CIDR_BLOCK&quot;,
      &quot;tcp_options&quot;: None,
      &quot;udp_options&quot;: {
        &quot;destination_port_range&quot;: {
          &quot;max&quot;: 53,
          &quot;min&quot;: 53
        },
        &quot;source_port_range&quot;: None
      }
    }

    ]
</code></pre><p></p><p>Build a new list of every rule (since we need to replace them all, even if they are not changing). Build a list of the rules we don&apos;t want to change, and append our changed rules defined above:</p><pre><code class="language-python"># test conditional update - skip existing with the same description
# try adding current_vcn_sl items one by one, *unless* their same &quot;description&quot; exists in sl_update_rules, then skip as we&apos;ll be updating it.
updateRuleDescriptions = &quot;&quot;
for i in sl_update_rules:
  updateRuleDescriptions += &quot;__&quot; + i[&apos;description&apos;] + &quot;__&quot;

logging.info(f&apos;Rules to update: {updateRuleDescriptions}&apos;)

# for a rule to be replaced, it must already exist with same description as above
newUpdateRulesString = &quot;[&quot; # string for now
for i in current_vcn_sl:
  if not (i.get(&apos;description&apos;) is None) and (i[&apos;description&apos;] in updateRuleDescriptions): # handle value &quot;None&quot; too
    # we&apos;re going to replace this one with an updated version, so skip it
    logging.info(f&quot;Skipping existing rule as will update/replace: {i[&apos;description&apos;]}&quot;)
    pass
  else:
    newUpdateRulesString = newUpdateRulesString + json.dumps(i) + &quot;,&quot;
    logging.info(f&quot;Keeping existing rule unmodified: {i[&apos;description&apos;]}&quot;)

# strip trailing comma, and append closing bracket
newUpdateRulesString = newUpdateRulesString[:-1] + &quot;]&quot;

# we now have a list of all existing rules, besides those we want to update - so append the new ones
newUpdateRules = json.loads(newUpdateRulesString) + sl_update_rules
</code></pre><p></p><p>Define a function to help us update the entire security list in the next step:</p><pre><code class="language-python">##################################################################
# API to update the security list doesn&#x2019;t expect a JSON object, 
#   so use this function to update the rules into something the API expects using IngressSecurityRule, 
#   this function would be called for every one of the rules
########################
def makeIngressRules(t):
    if t.get(&apos;tcp_options&apos;) is None:
       theseTcpOptions = None
    else:
        theseTcpOptions = oci.core.models.TcpOptions(
                    destination_port_range=oci.core.models.PortRange(
                        max=t[&apos;tcp_options&apos;][&apos;destination_port_range&apos;][&apos;max&apos;],
                        min=t[&apos;tcp_options&apos;][&apos;destination_port_range&apos;][&apos;min&apos;]),
                    )

    if t.get(&apos;udp_options&apos;) is None:
       theseUdpOptions = None
    else:
        theseUdpOptions = oci.core.models.UdpOptions(
                    destination_port_range=oci.core.models.PortRange(
                        max=t[&apos;udp_options&apos;][&apos;destination_port_range&apos;][&apos;max&apos;],
                        min=t[&apos;udp_options&apos;][&apos;destination_port_range&apos;][&apos;min&apos;]),
                    )

    if t.get(&apos;icmp_options&apos;) is None:
       theseIcmpOptions = None
    else:
        theseIcmpOptions = oci.core.models.IcmpOptions(
                        type=t[&apos;icmp_options&apos;][&apos;type&apos;],
                        code=t[&apos;icmp_options&apos;][&apos;code&apos;]
                        )

    return(oci.core.models.IngressSecurityRule(
        source=t[&apos;source&apos;],
        icmp_options=theseIcmpOptions,
        protocol=t[&apos;protocol&apos;],
        source_type=t[&apos;source_type&apos;],
        is_stateless=t[&apos;is_stateless&apos;],
        tcp_options=theseTcpOptions,
        udp_options=theseUdpOptions,
        description=t[&apos;description&apos;])
        )
</code></pre><p></p><p>Use the above function to update the security list with the complete list of all ingress rules, unchanged and changed:</p><pre><code class="language-python">updateReturn = core_client.update_security_list(
    security_list_id=vcn_sl_id,
    update_security_list_details=oci.core.models.UpdateSecurityListDetails(
        display_name=sl_name,
        ingress_security_rules=[makeIngressRules(t) for t in newUpdateRules]
))
</code></pre><p></p><p>Finally tidy up, delete the recorded IP file, save the new IP address for comparison on the next run.</p><pre><code class="language-python">if (updateReturn.status == 200):
  # delete IP file
  logging.info(f&quot;Deleting IP file&quot;)
  try:
    os.remove(ipFilename)
  except:
    logging.info(f&quot;Failed to remove IP file.&quot;)

  # log IP just updated, so next time we can check any new IPs against this, and only update FW if they differ
  logging.info(f&apos;Updating last change IP file&apos;)
  try:
    with open(lastIPFilename, &quot;w&quot;) as updateFile:
      updateFile.write(savedIP)
      logging.info(&quot;Updated last updated IP file.&quot;)
  except:
    logging.error(&quot;Failed to update last updated IP file&quot;)
    
else:
  logging.info(f&quot;Final status not 200, not deleting IP file or updating last updated file.&quot;)
</code></pre><p></p><h2 id="step-4schedule-the-script-to-run-periodically">Step 4 - Schedule the Script to run periodically</h2><p>A cron job is probably easiest, something like</p><pre><code class="language-bash"># every minute, look for new IP address file and update FW rules
* * * * *       /home/ubuntu/XXX/YYY.py &gt;&gt; /home/ubuntu/XXX/YYY.log 2&gt;&amp;1
</code></pre><p></p><h2 id="references">References</h2><ul><li>Python SDK for Oracle Cloud Infrastructure <a href="https://docs.oracle.com/en-us/iaas/tools/python/2.95.0/">main page</a> (v2.95.0 is latest at as March 2023). An excellent resource, start with the <a href="https://docs.oracle.com/en-us/iaas/tools/python/2.95.0/installation.html#install">installation instructions</a> / <a href="https://docs.oracle.com/en-us/iaas/tools/python/2.95.0/quickstart.html#id1">quickstart </a>sections.</li></ul>]]></content:encoded></item><item><title><![CDATA[ChatGPT Writes a Story with Jargon references]]></title><description><![CDATA[<p>I asked ChatGPT to &quot;<em>Write me a story about the early days of computing, using as many terms from the <a href="https://jargonfile.johnswitzerland.com/">Jargon File</a> as possible</em>&quot;. This was the first result.</p><hr><p>In the early days of computing, the world was a different place. There were no smartphones, no laptops, and</p>]]></description><link>https://blog.johnswitzerland.com/chatgpt-writes-a-story-with-jargon-references/</link><guid isPermaLink="false">640260f80a60c30001085a6f</guid><dc:creator><![CDATA[John Switzerland]]></dc:creator><pubDate>Sat, 04 Mar 2023 16:40:51 GMT</pubDate><media:content url="https://blog.johnswitzerland.com/content/images/2023/03/markus-spiske-gcgves5H_Ac-unsplash.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://blog.johnswitzerland.com/content/images/2023/03/markus-spiske-gcgves5H_Ac-unsplash.jpg" alt="ChatGPT Writes a Story with Jargon references"><p>I asked ChatGPT to &quot;<em>Write me a story about the early days of computing, using as many terms from the <a href="https://jargonfile.johnswitzerland.com/">Jargon File</a> as possible</em>&quot;. This was the first result.</p><hr><p>In the early days of computing, the world was a different place. There were no smartphones, no laptops, and no Internet. Instead, there were giant <a href="https://jargonfile.johnswitzerland.com/mainframe.html">mainframe </a>computers, <a href="https://jargonfile.johnswitzerland.com/blinkenlights.html">blinking lights</a>, and <a href="https://jargonfile.johnswitzerland.com/punched-card.html">punch cards</a>.</p><p>One of the pioneers of this era was a man named Bill. He was a <a href="https://jargonfile.johnswitzerland.com/hacker.html">hacker</a>, in the true sense of the word. He loved nothing more than tinkering with computers, exploring their inner workings, and pushing them to their limits.</p><p>Bill worked at a university, where he had access to one of the most powerful <a href="https://jargonfile.johnswitzerland.com/mainframe.html">mainframes </a>in the world. He spent countless hours in the machine room, typing away at the <a href="https://jargonfile.johnswitzerland.com/console.html">console</a>, surrounded by the hum of the cooling fans.</p><p>At first, Bill was just a <a href="https://jargonfile.johnswitzerland.com/neophilia.html">neophyte</a>, struggling to understand the arcane commands and cryptic error messages that filled the screen. But he was determined to learn, and he quickly became a <a href="https://jargonfile.johnswitzerland.com/wizard.html">wizard </a>with the tools of his trade.</p><p>He learned to program in a variety of languages, from <a href="https://jargonfile.johnswitzerland.com/Fortrash.html">FORTRAN </a>to assembly. He wrote elegant, efficient code that could do things that no one had ever thought possible. He created elaborate algorithms, <a href="https://jargonfile.johnswitzerland.com/recursion.html">recursive </a>functions, and elegant data structures.</p><p>But Bill didn&apos;t just write code for its own sake. He was a <a href="https://jargonfile.johnswitzerland.com/hacker.html">hacker </a>with a cause. He wanted to use his skills to make the world a better place. He believed that computers could be a powerful tool for social change, if only they were used correctly.</p><p>So he started working on a project that he called the <a href="https://jargonfile.johnswitzerland.com/hacker-ethic.html">&quot;Hackers&apos; Manifesto.&quot;</a> It was a bold vision for a future in which everyone had access to computers, and in which the power of technology was harnessed for the greater good.</p><p>To make this vision a reality, Bill knew that he would need to recruit other like-minded <a href="https://jargonfile.johnswitzerland.com/hacker.html">hackers</a>. And so he began to <a href="https://jargonfile.johnswitzerland.com/the-network.html">network</a>, using the primitive tools of the day: <a href="https://jargonfile.johnswitzerland.com/BBS.html">bulletin board systems</a>, <a href="https://jargonfile.johnswitzerland.com/Usenet.html">usenet</a>, and <a href="https://jargonfile.johnswitzerland.com/email.html">email</a>.</p><p>He met other <a href="https://jargonfile.johnswitzerland.com/hacker.html">hackers </a>who shared his passion, and together they formed a loose-knit community that spanned the globe. They called themselves the &quot;Netizens,&quot; and they believed that the <a href="https://jargonfile.johnswitzerland.com/Internet.html">Internet </a>was the key to a brighter future.</p><p>Together, the Netizens worked on a variety of projects. They created software tools that made it easier to use computers. They built <a href="https://jargonfile.johnswitzerland.com/the-network.html">networks</a> that allowed people to communicate with each other from anywhere in the world. And they fought against the forces of corporate greed and government censorship that threatened to stifle innovation and creativity.</p><p>In the end, the Netizens&apos; vision prevailed. Today, we live in a world that is more connected, more open, and more innovative than ever before. And it all started with a few intrepid <a href="https://jargonfile.johnswitzerland.com/hacker.html">hackers</a>, like Bill, who saw the potential of computers and dared to dream of a better future.</p>]]></content:encoded></item><item><title><![CDATA[Regain Access to Oracle Instance]]></title><description><![CDATA[<p>If you lose access to an instance (e.g. by forgetting/losing your SSH keys, or a server update breaks SSH access due to file permission changes) then you can try the following options, in order of increasing effort.</p><p>For future reference, tailscale&apos;s wonderful <a href="https://tailscale.com/kb/1193/tailscale-ssh/">ssh feature</a> would mitigate</p>]]></description><link>https://blog.johnswitzerland.com/regain-access-to-oracle-instance/</link><guid isPermaLink="false">6401bcd30a60c30001085a2a</guid><category><![CDATA[oracle]]></category><category><![CDATA[free-tier]]></category><dc:creator><![CDATA[John Switzerland]]></dc:creator><pubDate>Fri, 03 Mar 2023 09:39:06 GMT</pubDate><media:content url="https://blog.johnswitzerland.com/content/images/2023/03/akhilesh-sharma-0UQWosGper4-unsplash.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://blog.johnswitzerland.com/content/images/2023/03/akhilesh-sharma-0UQWosGper4-unsplash.jpg" alt="Regain Access to Oracle Instance"><p>If you lose access to an instance (e.g. by forgetting/losing your SSH keys, or a server update breaks SSH access due to file permission changes) then you can try the following options, in order of increasing effort.</p><p>For future reference, tailscale&apos;s wonderful <a href="https://tailscale.com/kb/1193/tailscale-ssh/">ssh feature</a> would mitigate this too since accessing your ssh server via tailscale&apos;s ssh feature safely bypasses key authentication anyway.</p><h3 id="use-the-oci-console-to-login">Use the OCI Console to login</h3><p>If you <a href="https://blog.johnswitzerland.com/oracle-free-tier-tailscale/">followed best practices</a> and created a password for a local account (with sudo rights) then use the OCI Console to connect to your instance from a browser, and fix the issue.</p><h3 id="boot-into-single-user-mode">Boot into Single User Mode</h3><p>You can use the instance&apos;s serial console to boot into Single User Mode, and from there you should have the access to fix whatever needs fixing. </p><p>Follow the instructions in Oracle&apos;s documentation:</p><ul><li><a href="https://docs.oracle.com/en-us/iaas/Content/Compute/References/serialconsole.htm">Troubleshooting Instances Using Instance Console Connections</a></li></ul><h3 id="use-a-bastion">Use a Bastion</h3><p>Thanks to reddit user <a href="https://www.reddit.com/user/Need2Survive/">Need2Survive</a>, this works nicely (<a href="https://www.reddit.com/r/oraclecloud/comments/11ge40d/is_there_a_way_to_add_ssh_key_to_an_existing/">original post</a>):</p><ul><li>Create a bastion in the same VCN (it&apos;s free)</li><li>Create a bastion session into the corrupted host by generating or adding a known key</li><li>Copy the SSH command generated and connect by replacing the &lt;privateKey&gt; part (2 times)</li><li>You will be logged into the instance.</li><li>Fix the issue(s) e.g. by editing /home/opc/.ssh/authorized_keys</li></ul><h3 id="using-another-instance">Using Another Instance</h3><p>the least fun of all, but good as a last resort (assuming you can spin up a new instance or reuse an existing one). Again thanks to user <a href="https://www.reddit.com/user/Need2Survive/">Needs2Survive</a>.</p><ul><li>Terminate instance preserving boot volume</li><li>Bring up another Linux instance from scratch</li><li>Attach the boot volume as additional volume, run attach commands and mount it</li><li>Edit the file /home/opc/.ssh/authorized_keys to add the required key</li><li>Disconnect and detach the additional volume</li><li>Terminate the second instance</li><li>Create new instance from the fixed boot volume</li></ul><h3 id="oracle-references">Oracle References</h3><ul><li><a href="https://blogs.oracle.com/cloud-infrastructure/post/recovering-opc-user-ssh-key-on-oracle-cloud-infrastructure">https://blogs.oracle.com/cloud-infrastructure/post/recovering-opc-user-ssh-key-on-oracle-cloud-infrastructure</a></li></ul>]]></content:encoded></item><item><title><![CDATA[Setting up Ghost]]></title><description><![CDATA[<p>Assuming you&apos;re using a private Ubuntu Server, Ghost will run in a container for convenience, with a local MySQL instance, and Apache2 as a reverse proxy.</p><h3 id="step-1install-docker">Step 1 - install docker </h3><p>Instructions at <a href="https://docs.docker.com/engine/install/ubuntu/">https://docs.docker.com/engine/install/ubuntu/</a>, installing from docker&apos;s apt repo:</p><pre><code class="language-bash">sudo</code></pre>]]></description><link>https://blog.johnswitzerland.com/ghost-setup-part-1-container/</link><guid isPermaLink="false">63bf24e1a2a46800014cd761</guid><dc:creator><![CDATA[John Switzerland]]></dc:creator><pubDate>Tue, 07 Feb 2023 10:51:49 GMT</pubDate><media:content url="https://blog.johnswitzerland.com/content/images/2023/02/tandem-x-visuals-FZOOxR2auVI-unsplash.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://blog.johnswitzerland.com/content/images/2023/02/tandem-x-visuals-FZOOxR2auVI-unsplash.jpg" alt="Setting up Ghost"><p>Assuming you&apos;re using a private Ubuntu Server, Ghost will run in a container for convenience, with a local MySQL instance, and Apache2 as a reverse proxy.</p><h3 id="step-1install-docker">Step 1 - install docker </h3><p>Instructions at <a href="https://docs.docker.com/engine/install/ubuntu/">https://docs.docker.com/engine/install/ubuntu/</a>, installing from docker&apos;s apt repo:</p><pre><code class="language-bash">sudo apt-get update

sudo apt-get install \
    ca-certificates \
    curl \
    gnupg \
    lsb-release

sudo mkdir -p /etc/apt/keyrings

curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg

echo \
  &quot;deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
  $(lsb_release -cs) stable&quot; | sudo tee /etc/apt/sources.list.d/docker.list &gt; /dev/null

sudo apt-get update

sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin

</code></pre><p>Test docker with: </p><pre><code class="language-bash">sudo docker run hello-world
</code></pre><h3 id="step-2setup-mysql-user-and-db">Step 2 - Setup MySQL User and DB</h3><p>Create a user &quot;ghost&quot;, a database with the same name, and give the ghost user all rights to the ghost database:</p><pre><code class="language-sql">CREATE USER &apos;ghost&apos;@&apos;localhost&apos; IDENTIFIED BY &apos;passwordxxx&apos;;
create database ghost;
GRANT ALL PRIVILEGES ON ghost.* TO &apos;ghost&apos;@&apos;%&apos;;</code></pre><h3 id="step-3configure-mysql-to-listen-to-docker-gateway-ip">Step 3 - Configure MySQL to listen to Docker gateway IP</h3><p>For containers to see services on the host, like MySQL / MariaDB, those services need to listen to the Docker network&apos;s gateway IP - 172.17.0.1 by default.</p><p>For MySQL, edit the MySQL config file:</p><pre><code class="language-bash">sudo nano /etc/mysql/mysql.conf.d/mysqld.cnf</code></pre><p>and change &quot;bind-address&quot; to append the gateway IP address:</p><pre><code class="language-config">bind-address            = 127.0.0.1,172.17.0.1</code></pre><p>Some older versions of MySQL don&apos;t support multiple different listening addresses, in which case YMMV.</p><h3 id="step-4setup-the-ghost-docker-container">Step 4 - setup the Ghost docker container</h3><p>Create a folder to store Ghost content outside container, and the docker compose YAML config file:</p><pre><code class="language-bash">mkdir ~/ghost</code></pre><p>and then create ghost.yaml in folder, replacing XXX as required (assuming you&apos;re using a local MySQL database, with a database user &quot;ghost&quot; and database named &quot;ghost&quot;, setup in the next step):</p><pre><code class="language-yaml">version: &apos;3.1&apos;

services:

  ghost:
    image: ghost:latest
    restart: always
    ports:
      - 2368:2368
    volumes:
      - /XXX/ghost/content:/var/lib/ghost/content:z
    environment:
      # see https://ghost.org/docs/config/#configuration-options
      database__client: mysql
      database__connection__host: 172.17.0.1
      database__connection__user: ghost
      database__connection__password: passwordxxx
      database__connection__database: ghost
      url: https://blog.johnswitzerland.com
      # defaults to PROD env unless you override here
      #NODE_ENV: development</code></pre><p>Use docker compose to fetch, configure and start the latest Ghost container image:</p><pre><code class="language-bash">docker compose -f ghost.yaml up</code></pre><h3 id="step-5apache-as-a-reverse-proxy">Step 5 - Apache as a Reverse Proxy</h3><p>I chose Apache2, NGINX is also an easy substitute.</p><p>Apache needs the following modules enabled:</p><ul><li>headers</li><li>proxy_http</li></ul><p>And config for the site needs to include something like this:</p><pre><code class="language-apacheconf">        RequestHeader set X-Forwarded-Proto &quot;https&quot;
        SSLProxyEngine on

        ProxyRequests Off
        &lt;Proxy *&gt;
          Order deny,allow
          Allow from all
        &lt;/Proxy&gt;

        ProxyPreserveHost On
        ProxyPass &quot;/&quot; &quot;http://localhost:2368/&quot;
        ProxyPassReverse &quot;/&quot; &quot;http://localhost:2368/&quot;

        &lt;Location /&gt;
          Order allow,deny
          Allow from all
        &lt;/Location&gt;
</code></pre><p>Where:</p><ol><li>RequestHeader is required to avoid Ghost redirect loops (endless 301/302 responses) - letting Ghost know Apache is handling SSL and not to try and redirect to a secure link</li><li>ProxyPreserveHost is required to pass your full Ghost public domain name to Ghost, rather than the internal address specified by ProxyPass</li></ol>]]></content:encoded></item><item><title><![CDATA[Oracle Free Tier references & resources]]></title><description><![CDATA[<p>Links to handy references. Oracle&apos;s documentation is generally excellent, worth looking before asking.</p><h2 id="general">General</h2><p><a href="https://www.reddit.com/r/oraclecloud/">/r/oraclecloud</a> is a great place to search for answers and ask questions, plenty of helpful souls around.</p><h2 id="oracle">Oracle </h2><!--kg-card-begin: markdown--><ol>
<li>Free Tier docs <a href="https://docs.oracle.com/en-us/iaas/Content/FreeTier/freetier.htm">top level</a>
<ul>
<li><a href="https://docs.oracle.com/en-us/iaas/Content/FreeTier/freetier_topic-Always_Free_Resources.htm">Always Free Resources</a> section (including specifics of what counts as</li></ul></li></ol>]]></description><link>https://blog.johnswitzerland.com/oracle-free-tier-references/</link><guid isPermaLink="false">63d920030121b00001b337bd</guid><category><![CDATA[free-tier]]></category><category><![CDATA[oracle]]></category><dc:creator><![CDATA[John Switzerland]]></dc:creator><pubDate>Tue, 31 Jan 2023 14:55:54 GMT</pubDate><media:content url="https://blog.johnswitzerland.com/content/images/2023/01/gokhan-polat-iEioyOAmOeQ-unsplash.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://blog.johnswitzerland.com/content/images/2023/01/gokhan-polat-iEioyOAmOeQ-unsplash.jpg" alt="Oracle Free Tier references &amp; resources"><p>Links to handy references. Oracle&apos;s documentation is generally excellent, worth looking before asking.</p><h2 id="general">General</h2><p><a href="https://www.reddit.com/r/oraclecloud/">/r/oraclecloud</a> is a great place to search for answers and ask questions, plenty of helpful souls around.</p><h2 id="oracle">Oracle </h2><!--kg-card-begin: markdown--><ol>
<li>Free Tier docs <a href="https://docs.oracle.com/en-us/iaas/Content/FreeTier/freetier.htm">top level</a>
<ul>
<li><a href="https://docs.oracle.com/en-us/iaas/Content/FreeTier/freetier_topic-Always_Free_Resources.htm">Always Free Resources</a> section (including specifics of what counts as &quot;free tier&quot;)</li>
<li>Unused instances can be reclaimed by Oracle, and <a href="https://docs.oracle.com/en-us/iaas/Content/FreeTier/freetier_topic-Always_Free_Resources.htm#compute__idleinstances">what defines &quot;unused&quot;</a></li>
<li>Incoming bandwidth is free, and a very generous 10 TB outbound data a month. You can track it via <strong>Billing &amp; Cost Management</strong> --&gt; <strong>Cost Analysis</strong> (Show: Usage, Filter --&gt; Service, Network)</li>
</ul>
</li>
<li>OCI Free Tier <a href="https://www.oracle.com/cloud/free/faq/">Frequently Asked Questions</a>
<ul>
<li>Accepted payment methods in particular state &quot;<em>We accept credit cards and debit cards that function like credit cards. We do not accept debit cards with a PIN, virtual cards, or prepaid cards.</em>&quot;</li>
</ul>
</li>
<li>If you go paid (PAYG highly recommended for free tier users), the <a href="https://www.oracle.com/cloud/costestimator.html">Oracle Cost Estimator</a> is excellent
<ul>
<li>You can also easily set up a cost alert, for both actual and projected spend, for extra peace of mind.</li>
</ul>
</li>
<li>After switching to PAYG, enabling 2FA / MFA on your logins - especially those with OCI admin access - is crucial. You don&apos;t want anyone creating instances and costing you money, or getting access to existing instances. See the well written IAM MFA (Identity &amp; Access Management - Multi-Factor Authentication) <a href="https://docs.oracle.com/en-us/iaas/Content/Security/Reference/iam_security.htm#iam_mfa_best_practice">guide</a>
<ul>
<li><a href="https://www.reddit.com/user/threehappypenguins/">u/threehappypenguins</a> recently shared the process they followed here:</li>
<li>Sign into the console</li>
<li>Menu &gt; Identity &amp; Security &gt; Users</li>
<li>Federation &gt; OracleIdentityCloudService</li>
<li>find the link to &quot;Oracle Identity Cloud Service Console&quot; in the Identity Provider Information section</li>
<li>Security section / menu item</li>
<li>followed steps 7-10 from <a href="https://stackoverflow.com/a/68104702/3323648">here</a> (a few things are different, but it&apos;s generally the same).</li>
<li>Sign in again and you should be prompted to setup MFA.</li>
</ul>
</li>
<li><a href="https://community.oracle.com/customerconnect/">Cloud Customer Connect</a> community</li>
<li>TOS / Terms of Service - what not to do with your free resources if you want to avoid your instances / account being terminated:<br>
7. <a href="https://www.oracle.com/assets/cloud-csa-v012418-sa-eng-4419927.pdf">TOS document</a><br>
8. <a href="https://www.reddit.com/r/oraclecloud/comments/10gamzu/how_not_to_get_your_account_terminated/">Good discussion / breakdown of the above</a></li>
<li><a href="https://ocistatus.oraclecloud.com/#/">OCI status page</a></li>
</ol>
<!--kg-card-end: markdown--><p></p>]]></content:encoded></item><item><title><![CDATA[ChatGPT Poem on Appreciation]]></title><description><![CDATA[<p>My son asked ChatGPT to &quot;<em>write a poem about appreciation as a kid</em>&quot;:</p><p>Nature&apos;s canvas, painted bright,<br>With colors that are pure and light,<br>Each leaf and petal, every tree,<br>A masterpiece for you and me.</p><p>The sun, it rises in the east,<br>And sets in</p>]]></description><link>https://blog.johnswitzerland.com/chatgpt-poem-on-appreciation/</link><guid isPermaLink="false">63d1814cd4f00e00012d3378</guid><category><![CDATA[AI]]></category><dc:creator><![CDATA[John Switzerland]]></dc:creator><pubDate>Wed, 25 Jan 2023 19:23:56 GMT</pubDate><media:content url="https://blog.johnswitzerland.com/content/images/2023/02/jason-leung-HBGYvOKXu8A-unsplash.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://blog.johnswitzerland.com/content/images/2023/02/jason-leung-HBGYvOKXu8A-unsplash.jpg" alt="ChatGPT Poem on Appreciation"><p>My son asked ChatGPT to &quot;<em>write a poem about appreciation as a kid</em>&quot;:</p><p>Nature&apos;s canvas, painted bright,<br>With colors that are pure and light,<br>Each leaf and petal, every tree,<br>A masterpiece for you and me.</p><p>The sun, it rises in the east,<br>And sets in the west, with perfect peace,<br>It warms the earth and lights the way,<br>For all that live and grow each day.</p><p>The moon and stars, they shine so bright,<br>A beacon in the dark of night,<br>They guide us through the darkest hours,<br>And light the path to better powers.</p><p>So let us take a moment now,<br>To gaze upon this beauty, wow,<br>For nature&apos;s art is all around,<br>And always here, to astound.</p>]]></content:encoded></item><item><title><![CDATA[Installing CrowdSec on Ubuntu]]></title><description><![CDATA[<!--kg-card-begin: markdown--><blockquote>
<p>&quot;CrowdSec offers a crowd-based cyber security suite to protect your online services, visualize &amp; act upon threats, and a TIP (Threat Intel Platform) to block malicious IPs.&quot;</p>
</blockquote>
<!--kg-card-end: markdown--><h3 id="why-crowdsec">Why CrowdSec</h3><p>From your perspective, <a href="https://www.crowdsec.net/">CrowdSec </a>helps protect your public-facing server(s) from malicious scanners trying to abuse open ports, brute</p>]]></description><link>https://blog.johnswitzerland.com/installing-crowd/</link><guid isPermaLink="false">63c532db4611440001cfd2e1</guid><category><![CDATA[security]]></category><category><![CDATA[ubuntu]]></category><dc:creator><![CDATA[John Switzerland]]></dc:creator><pubDate>Wed, 18 Jan 2023 10:26:38 GMT</pubDate><media:content url="https://blog.johnswitzerland.com/content/images/2023/01/henry-hustava-j_Ch0mwBNds-unsplash.jpg" medium="image"/><content:encoded><![CDATA[<!--kg-card-begin: markdown--><blockquote>
<img src="https://blog.johnswitzerland.com/content/images/2023/01/henry-hustava-j_Ch0mwBNds-unsplash.jpg" alt="Installing CrowdSec on Ubuntu"><p>&quot;CrowdSec offers a crowd-based cyber security suite to protect your online services, visualize &amp; act upon threats, and a TIP (Threat Intel Platform) to block malicious IPs.&quot;</p>
</blockquote>
<!--kg-card-end: markdown--><h3 id="why-crowdsec">Why CrowdSec</h3><p>From your perspective, <a href="https://www.crowdsec.net/">CrowdSec </a>helps protect your public-facing server(s) from malicious scanners trying to abuse open ports, brute force passwords, scan webservers for vulnerabilities - and generally keeps your logfiles free of most junk. It is both proactive and reactive. </p><p>You also get a great free dashboard which lets you see CrowdSec in action across all your servers.</p><p>From a community perspective, it uses collected info from all users to improve everyone&apos;s protection dynamically (far better than an isolated reactive-only tool like <em>Fail2Ban</em>) - and also as a bonus hurts dodgy actors by forcing them to change IP addresses. </p><h3 id="installing-crowdsec-on-ubuntu-server">Installing CrowdSec on Ubuntu Server </h3><p>Follow the install instructions at <a href="https://docs.crowdsec.net/docs/getting_started/install_crowdsec">https://docs.crowdsec.net/docs/getting_started/install_crowdsec</a></p><pre><code class="language-bash"># Step 1 - add crowdsec repos for easy updates/removal
curl -s https://packagecloud.io/install/repositories/crowdsec/crowdsec/script.deb.sh | sudo bash

# step 2 - install crowdsec detection (monitor attempts by parsing logfiles) 
apt install crowdsec

# step 3 - install a bouncer so crowdsec can take actions to block etc
apt install crowdsec-firewall-bouncer-iptables
</code></pre><p>That will instal the most common firewall bouncer (using iptables to block offending IP addresses dynamically). There are also tons of blockers for use in your applications themselves (e.g. wordpress, PHP) or to help with blocking for specific providers like CloudFlare or AWS. </p><h3 id="whitelist-your-own-ips">Whitelist your own IPs</h3><p>Whitelisting your own addresses is suggested - handy to know you won&apos;t be locked out of your own webserver when you do a mass vulnerability scan for example :-)</p><p>Create / edit <em>/etc/crowdsec/parsers/s02-enrich/mywhitelists.yaml</em> and add something like:</p><pre><code class="language-yaml">name: crowdsecurity/whitelists
description: &quot;Whitelist events from my fixed ip addresses&quot;
whitelist:
  reason: &quot;thine own ip ranges&quot;
  ip:
    - &quot;100.xxx.xxx.xxx&quot;
    - &quot;100.xxx.xxx.xxx&quot;
    - &quot;100.xxx.xxx.xxx&quot;
    - &quot;100.xxx.xxx.xxx&quot;
</code></pre><p>and then restart the crowdsec service</p><pre><code class="language-bash">sudo service crowdsec restart</code></pre><h3 id="whitelist-google-and-other-search-engine-ips">Whitelist Google and other Search Engine IPs</h3><p>You probably also want to ensure that search engines are whitelisted to avoid any chance of accidentally blocking good spiders / indexing services.</p><p>Install the &quot;<a href="https://hub.crowdsec.net/author/crowdsecurity/configurations/seo-bots-whitelist">SEO Bots Whitelist</a>&quot; postoverflow (which does have a <em>slight </em>performance impact):</p><pre><code class="language-bash">cscli postoverflows install crowdsecurity/seo-bots-whitelist
sudo service crowdsec restart</code></pre><h3 id="testing-crowdsec">Testing Crowdsec</h3><p>If you&apos;ve got everything running and want to test your setup is working, one easy way is to <strong>temporarily </strong>block your own IP address, and then confirm access is blocked (e.g. website won&apos;t load for you):</p><pre><code class="language-bash">cscli decisions add --ip 1.2.3.4 --duration 5m --reason &quot;Bouncer ban test&quot;</code></pre><h3 id="handy-cli-cheat-sheet">Handy CLI cheat sheet </h3><pre><code class="language-bash"># pretty-list all installed collections
sudo cscli hub list

# list active decisions (append --ip to be specific)
sudo cscli decisions list

# list alerts (stay around after decisions expire)
sudo cscli alerts list

# list ipset IPs currently in place for blocking (by bouncer).
# pipe to &quot;head&quot; to see summary as list can be looooong
sudo ipset list crowdsec-blacklists | less

# to see the ipset list DROP rule
sudo iptables -S | grep crowdsec

# check list of bouncers
sudo cscli bouncers list

# update hub index
sudo cscli hub update
# and then upgrade collections/parsers/scenarios/overflows
sudo cscli hub upgrade

# rerun the setup wizard at any time (attempt to autodetect collections etc)
/usr/share/crowdsec/wizard.sh -c

# Test a logfile entry against the current setup to see what happens
sudo cscli explain --log &quot;Jan 16 00:08:39 oracle sshd[49646]: Disconnected from authenticating user root 36.89.217.30 port 55820 [preauth]&quot; --type syslog
</code></pre><h3 id="troubleshooting">Troubleshooting</h3><ol><li>check both the <em>crowdsec</em> and <em>crowdsec-firewall-bouncer</em> services (and any other bouncers you may have installed) are running</li><li>Check the logs - /var/log/crowdsec*</li><li>Use the CLI&apos;s <a href="https://docs.crowdsec.net/docs/cscli/cscli_explain">explain </a>functionality to see why something is or isn&apos;t being flagged (example above in the cheat-sheet)</li><li>The <a href="https://www.crowdsec.net/blog/crowdsec-on-discord">CrowdSec discord</a> server is full of helpful people willing to assist, and also a great resource to search for any issues you may be having (from basic through to advanced custom scenarios)</li></ol><p></p><p></p><p></p><p></p>]]></content:encoded></item><item><title><![CDATA[Oracle Free Tier VPS - Best Practices]]></title><description><![CDATA[<p>Oracle offers a very <a href="https://docs.oracle.com/en-us/iaas/Content/FreeTier/freetier_topic-Always_Free_Resources.htm">generous free tier</a> (although a little too generous perhaps which has led to temporary capacity issues in some regions - see <a href="https://www.oracle.com/cloud/free/faq/">FAQ</a>).</p><!--kg-card-begin: markdown--><blockquote>
<p>&quot;Each tenancy gets the first 3,000 OCPU hours and 18,000 GB hours per month for free to create Ampere A1 Compute</p></blockquote>]]></description><link>https://blog.johnswitzerland.com/oracle-free-tier-tailscale/</link><guid isPermaLink="false">63c0fa31a2a46800014cd837</guid><category><![CDATA[oracle]]></category><category><![CDATA[tailscale]]></category><category><![CDATA[ubuntu]]></category><category><![CDATA[free-tier]]></category><dc:creator><![CDATA[John Switzerland]]></dc:creator><pubDate>Fri, 13 Jan 2023 14:44:56 GMT</pubDate><media:content url="https://blog.johnswitzerland.com/content/images/2023/01/OCI-tailscale-1.png" medium="image"/><content:encoded><![CDATA[<img src="https://blog.johnswitzerland.com/content/images/2023/01/OCI-tailscale-1.png" alt="Oracle Free Tier VPS - Best Practices"><p>Oracle offers a very <a href="https://docs.oracle.com/en-us/iaas/Content/FreeTier/freetier_topic-Always_Free_Resources.htm">generous free tier</a> (although a little too generous perhaps which has led to temporary capacity issues in some regions - see <a href="https://www.oracle.com/cloud/free/faq/">FAQ</a>).</p><!--kg-card-begin: markdown--><blockquote>
<p>&quot;Each tenancy gets the first 3,000 OCPU hours and 18,000 GB hours per month for free to create Ampere A1 Compute instances using the VM.Standard.A1.Flex shape (equivalent to 4 OCPUs and 24 GB of memory). Each tenancy also gets two VM.Standard.E2.1.Micro instances for free&quot; --Oracle</p>
</blockquote>
<!--kg-card-end: markdown--><p>I have the following instances on the free tier:</p><ul><li>2x VM.Standard.E2.1.Micro instances (AMD processor - each with 1 OCPU + 1GB RAM) running Ubuntu Minimal. Good for smaller workloads.</li><li>2x VM.Standard.A1.Flex instances (ARM processor - each with 2 OCPUs and 12GB RAM) running Ubuntu Server. Great performance.</li></ul><p>While this hasn&apos;t happened to me personally, I have seen many people locked out of their instances for various reasons, which can result in data loss or the loss of the instance if you have to delete it and then run into capacity issues in your region. So make sure you have a backup access plan...</p><hr><h3 id="simple-password-enabled-user-account">Simple: Password Enabled User Account</h3><p>Make sure you have a local user account which has sudo privileges and a password set. In an emergency, you can use the OCI Console Connection to login to your instances from the web.</p><p>I suggest using an account other than the default &quot;ubuntu&quot; (assuming youre using an Ubuntu imge) for security reasons, e.g. the default &quot;opc&quot; account - and then be sure to test it works before moving on:</p><pre><code class="language-bash">sudo passwd opc
sudo usermod -aG sudo opc</code></pre><p>I also recommend ensuring you use certificates for SSH authentication, not passwords. If you do use passwords for some reason, ensure the above account is not permitted to login.</p><hr><h3 id="better-enable-tailscale-ssh-on-your-instances">Better: Enable Tailscale SSH on your instances</h3><p><a href="https://tailscale.com/">Tailscale </a>is pure magic freely available to everyone :-)</p><p>I strongly recommend setting up a Tailnet and adding all your instances to it with Tailscale SSH enabled. This lets you SSH securely into your instances from any of your Tailscale machines (linux, windows, mac) without needing a separate password or certificate - and as if that isn&apos;t enough, you can even SSH into your instances from the web!</p><p>Once you have a Tailscale account, register each instance (and enable SSH access):</p><pre><code class="language-bash">sudo tailscale up --ssh</code></pre><p>then from any other machine on your Tailscale network (windows, mac, linux) you can connect to the instance (without needing a password or certificate):</p><pre><code class="language-bash">ssh &lt;tailscale-machine-ip&gt;</code></pre><!--kg-card-begin: markdown--><blockquote>
<p><strong>Note</strong>: You might want to <a href="https://tailscale.com/kb/1028/key-expiry/">disable Tailscale key expiry</a> on your instances - otherwise you&apos;ll need to reauthenticate each of them after the default 180 days, which may be an issue if the only way you can connect is via Tailscale - see the first recommendation above for enabling OCI Console connections).</p>
</blockquote>
<!--kg-card-end: markdown--><p></p><hr><h3 id="dont-let-your-instances-sit-idle-for-long">Don&apos;t Let Your Instances Sit Idle for Long</h3><p>Oracle will sometimes reclaim (free tier) instances which are idle for extended periods to free up scarce free-tier capacity.</p><p>Oracle <a href="https://docs.oracle.com/en-us/iaas/Content/FreeTier/freetier_topic-Always_Free_Resources.htm#compute__idleinstances">defines an instance as idle</a> if, during a 7-day period, these all apply:</p><!--kg-card-begin: markdown--><blockquote>
<ul>
<li>CPU utilization for the 95th percentile is less than 10%</li>
<li>Network utilization is less than 10%</li>
<li>Memory utilization is less than 10% (applies to A1 shapes only)</li>
</ul>
</blockquote>
<!--kg-card-end: markdown--><p>So make sure you actually USE them - setup <a href="https://blog.johnswitzerland.com/installing-crowd/">CrowdSec</a>, a simple website, a container running your favourite service - the options are endless. </p><p>You can also convert your account to PAYG to avoid idle instances being reclaimed - highly recommended, as you still pay nothing if you stay within free tier limits. </p><h3 id="back-up-your-content">Back Up Your Content</h3><p>This should be obvious, and applies to everything not just Oracle - <strong>back up your data </strong>(content, config, etc). Anything you&apos;d regret losing if you permanently lost access to your instances or account tomorrow - back it up.</p><p>Oracle allows multiple backups of each volume which don&apos;t count against your storage limit. And make you also backup offsite. </p><h3 id="enable-2fa">Enable 2FA</h3><p>Oracle pretty much insists on this nowadays for good reasons - so enable 2-factor authentication on your OCI account to protect your setup. </p><p>Process is fairly straightforward - see my 2FA notes on my <a href="https://blog.johnswitzerland.com/oracle-free-tier-references/">Oracle Free Tier &#xA0;Notes and References</a> page for a guide. </p><p>If you&apos;re paranoid (like me) you can also create a new local (non-federated) OCI user account which doesn&apos;t use 2FA for emergencies (with a very strong password, and only used in emergencies). Just to be sure to add it to the Administrators group.</p>]]></content:encoded></item></channel></rss>