<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://ohxodi.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://ohxodi.com/" rel="alternate" type="text/html" /><updated>2026-01-26T21:42:18+00:00</updated><id>https://ohxodi.com/feed.xml</id><title type="html">Ohxodi’s blog</title><subtitle>A very simple blog to share what I like. Topics are mainly related to IT and cybersecurity stuff.</subtitle><entry><title type="html">Secure web access with NGINX</title><link href="https://ohxodi.com/server/2026/01/26/nginx-server-side-auth.html" rel="alternate" type="text/html" title="Secure web access with NGINX" /><published>2026-01-26T20:30:00+00:00</published><updated>2026-01-26T20:30:00+00:00</updated><id>https://ohxodi.com/server/2026/01/26/nginx-server-side-auth</id><content type="html" xml:base="https://ohxodi.com/server/2026/01/26/nginx-server-side-auth.html"><![CDATA[<p><img src="/assets/door.jpg" alt="Door" title="Door" /></p>

<h2 id="introduction">Introduction</h2>

<p>When you want to be sure that the person accessing your website is the right one, the common solution is to implement inside the web application itself an authentication method to let user access to a restricted area. As a result, you have to code a whole authentication and user management module, which can be very complicated (fortunately, many frameworks can do it for you).</p>

<p>As we will see in this article, another way is to delegate the authentication process to the webserver or the reverse proxy. Furthermore, it could also be the opportunity to use a stronger authentication method.</p>

<h2 id="what-we-want-to-achieve">What we want to achieve</h2>

<p>Let say that we have a Fedora server on the Internet with NGINX installed. We want to expose the Fedora Webserver Test Page at the address https://nginx.test.</p>

<figure class="image">
  <img src="/assets/test-page.png" alt="Fedora Webserver Test Page" />
  <figcaption>Fedora Webserver Test Page</figcaption>
</figure>

<p>We won’t go into detail to set up such a webserver as this is a very common configuration. Many websites can help for that. We just give below the NGINX configuration file <code class="language-plaintext highlighter-rouge">test-page.conf</code> placed in <code class="language-plaintext highlighter-rouge">/etc/nginx/conf.d/</code> :</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>server {
    listen 443 ssl;
    server_name nginx.test;

    ssl_certificate /etc/pki/tls/certs/nginx.test.crt;
    ssl_certificate_key /etc/pki/tls/private/nginx.test.key;

    location / {
        root /usr/share/nginx/html;
    }
}
</code></pre></div></div>

<p>What we want to achieve now is to configure NGINX in order to restrict the access of this page to authorized users only.</p>

<h2 id="certificates-verification">Certificates verification</h2>

<p>When you perform a SSL/TLS connection to a website, your connection is considered secured because your browser has verified that the certificate of the website is legitimate. To keep it simple, the website has proved to a Certificate Authority (CA) that it was the legitimate owner of the domain name. In return, the CA has signed a certificate that the owner can store on the server alongside a protected private key and send it to each client trying to perform an SSL/TLS connection. As the client browser trusts the CA, it also trusts the certificate sent by the website. In other words, the client authenticates the server : we talk about <em>client-side authentication</em>. This is basicaly what you (your browser) do every day while surfing the Internet. This is also what we have implemented for the moment with our Fedora Webserver Test Page.</p>

<p>What we want to achieve is the exact opposite : we want the server to authenticate the user. We call this process <em>server-side authentication</em>. Let see how we can do that with NGINX.</p>

<h2 id="nginx-certificates-verification">NGINX certificates verification</h2>

<h3 id="create-the-ca-for-client-certificates">Create the CA for client certificates</h3>

<p>The first thing we have to do is to create a CA in order to be able afterward to create client certificates.</p>

<p>Some articles on the Internet say that this CA must be the same than the one used to sign the SSL/TLS server certificate. Fortunately it is not the case. Indeed, CA are sometimes only used to sign server certificates and can’t be used to sign client certificates. It is for instance <a href="https://letsencrypt.org/2025/05/14/ending-tls-client-authentication" target="_blank">the case with Let’s Encrypt</a>, one of the most used CA in the world for SSL/TLS server certificates. Otherwise, any client certificates signed by Let’s Encrypt would be recongnized as legitimate ones, which would not be helpfull to control access to our server. It is then more relevent to decorelate both CA and manage client certificates signature with a private CA.</p>

<p>Precisely, let’s create our private CA :</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>openssl genrsa <span class="nt">-des3</span> <span class="nt">-out</span> root_ca.key 4096
openssl req <span class="nt">-x509</span> <span class="nt">-new</span> <span class="nt">-sha256</span> <span class="nt">-key</span> root_ca.key <span class="nt">-subj</span> <span class="s2">"/C=US/ST=NY/O=nginx-test/CN=ca.nginx.test"</span> <span class="nt">-days</span> 365 <span class="nt">-out</span> root_ca.crt
</code></pre></div></div>

<p>Then, you can copy the newly created certificate in <code class="language-plaintext highlighter-rouge">/etc/pki/tls/certs/</code> directory.</p>

<h3 id="register-the-ca-in-nginx">Register the CA in NGINX</h3>

<p>This certificate must now be given to NGINX in order for it to verify that certificates sent by clients belong to our private CA.</p>

<p>You simply have to add the <code class="language-plaintext highlighter-rouge">ssl_verify_client</code> and <code class="language-plaintext highlighter-rouge">ssl_trusted_certificate</code> directives to the NGINX configuration file <code class="language-plaintext highlighter-rouge">test-page.conf</code> placed in <code class="language-plaintext highlighter-rouge">/etc/nginx/conf.d/</code> :</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>server {
    listen 443 ssl;
    server_name nginx.test;

    ssl_certificate /etc/pki/tls/certs/nginx.test.crt;
    ssl_certificate_key /etc/pki/tls/private/nginx.test.key;

    ssl_verify_client on;
    ssl_trusted_certificate /etc/pki/tls/certs/root_ca.crt;

    location / {
        root /usr/share/nginx/html;
    }
}
</code></pre></div></div>

<p>Restart NGINX service to take into account this new configuration.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>systemctl restart nginx
</code></pre></div></div>

<p>As explained in <a href="https://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_trusted_certificate" target="_blank">NGINX documentation</a>, we have used here <code class="language-plaintext highlighter-rouge">ssl_trusted_certificate</code> directive because we don’t want the server to send the CA certificate to the client. If it doesn’t matter for you or if you want to help the client to select the right certificate, you can use instead <code class="language-plaintext highlighter-rouge">ssl_client_certificate</code> directive which will at first send the CA certificate to the client.</p>

<h3 id="creating-the-client-certificates">Creating the client certificates</h3>

<p>Last but not least, we have to create the client certificate which will be used to authenticate against the server.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>openssl genrsa <span class="nt">-out</span> client.key 2048
openssl req <span class="nt">-new</span> <span class="nt">-sha256</span> <span class="nt">-key</span> client.key <span class="nt">-subj</span> <span class="s2">"/C=US/ST=NY/O=nginx-test/CN=client.nginx.test"</span> <span class="nt">-out</span> client.csr
openssl x509 <span class="nt">-req</span> <span class="nt">-in</span> client.csr <span class="nt">-CA</span> root_ca.crt <span class="nt">-CAkey</span> root_ca.key <span class="nt">-CAcreateserial</span> <span class="nt">-out</span> client.crt <span class="nt">-days</span> 365 <span class="nt">-sha256</span>
</code></pre></div></div>

<p>You must now export the client key and certificate in PKCS12 format in order to import them in your browser.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>openssl pkcs12 <span class="nt">-export</span> <span class="nt">-out</span> client.p12 <span class="nt">-inkey</span> client.key <span class="nt">-in</span> client.crt
</code></pre></div></div>

<p>You are now able to add the certificate and its key to your browser. For example in Firefox, go to <em>Settings</em> &gt; <em>Privacy and Security</em> &gt; <em>Security</em> &gt; <em>Certificates</em> &gt; <em>View Certificates</em> and click on <em>Import</em> in <em>Your Certificates</em> tab.</p>

<h3 id="connection-to-the-server">Connection to the server</h3>

<p>When you try to connect to https://nginx.test with Firefox, a pop-up will now ask you to select a client certificate. Select the right one if you already have imported other certificates.</p>

<p>You will then be able to access the Fedora Webserver Test Page. In case you have not selected the right certificate or if you refuse to give one, a 400 “Bad Request” error will be returned by the webserver.</p>

<h2 id="the-benefits-of-such-a-configuration">The benefits of such a configuration</h2>

<p>You would tell me why such a configuration is interesting whereas it is so easy and common nowadays to implement an authentication method ?</p>

<p>You would be right if your website is built over a framework like Symfony or Django. However, this is not always the case, especially when you serve a static website like the one in our example. This method let you secure the web access very quickly.</p>

<p>Moreover, when you are building your website, your authentication method could be not implemented yet or not enough configured or secured. This method let you choose who can access the website (for example another developer) while the work is still in progress.</p>

<p>Even if you have implemented and correctly configured an authentication method, this solution can be very helpfull in some case. Let say that a critical vulnerability has been discovered on your website but this one has to be kept online for some users. This method has two benifits in such a scenario :</p>
<ul>
  <li>the website can still be reached by some user ;</li>
  <li>the website is not anymore directly accessible to potential attackers as the authentication process is delegated to the webserver which is probably not concerned by the vulnerability and which is anyway easier to update.</li>
</ul>

<p>While it is more and more recommended to implement Multi-Factor Authentication (MFA) to reduce illegitimate accesses via usual and weak authentication method, <em>server-side authentication</em> is a way to do that. Indeed, MFA is the combination of two factors (<em>what you know</em>, <em>what you have</em>, <em>what you are</em>, …) and the method presented in this article can be considered as a second factor (<em>what you have</em> : a certificate) if a password authentication (<em>what you know</em> : a password) has already been implemented on the website.</p>

<p>Finally, the benefit of such a configuration can simply be the fact that the server verifies the client legitimacy to access the website via a strong authentication method, as it is already done nowadays by clients on almost every websites. This is why we also talk about mutual TLS authentication (mTLS). This can be usefull for entity not able to protect the access of one or several web applications behind a VPN but which is able to deliver client certificates.</p>

<h2 id="conclusion">Conclusion</h2>

<p>We have seen in this article that we can easily implement a strong authentication method with a simple webserver configuration and some certificates handling. We have also seen that, even if this method is not so much used on the Internet, <em>server-side authentication</em> has many benefits that improves the security of your website.</p>

<h2 id="sources">Sources</h2>
<ul>
  <li><a href="https://serverfault.com/questions/761280/nginx-does-not-prompt-for-client-ssl-certificate/764509#764509" target="_blank">NGINX does not prompt for client ssl certificate</a></li>
  <li><a href="https://github.com/nategood/blog/blob/master/content/articles/client-side-certificate-authentication-in-ngi.md" target="_blank">Client Side Certificate Auth in Nginx</a></li>
  <li><a href="https://dev.to/stjamlb/nginx-configuration-tips-for-secure-communication-enabling-mtls-and-checking-client-fingerprint-4jf3" target="_blank">Nginx Configuration Tips for Secure Communication: Enabling mTLS and checking client fingerprint</a></li>
</ul>]]></content><author><name></name></author><category term="Server" /><category term="nginx" /><category term="server" /><category term="tls" /><category term="authentication" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">Create and share virtual environments with Vagrant</title><link href="https://ohxodi.com/virtualization/2025/06/29/vagrant.html" rel="alternate" type="text/html" title="Create and share virtual environments with Vagrant" /><published>2025-06-29T20:30:00+00:00</published><updated>2025-06-29T20:30:00+00:00</updated><id>https://ohxodi.com/virtualization/2025/06/29/vagrant</id><content type="html" xml:base="https://ohxodi.com/virtualization/2025/06/29/vagrant.html"><![CDATA[<p><img src="/assets/boxes.jpg" alt="Tunnel" title="Boxes" /></p>

<h2 id="introduction">Introduction</h2>

<p>When prototyping or developing stuffs, it is often a good idea to use Virtual Machines (VMs) in order to isolate everything and avoid messing up your computer. You are then able to test something and recreate the machines when everything is broken.</p>

<p>However, creating VMs can be painful and repeating this process several times can be very long. Using tools and concepts close to Infrastructure as Code (IaC) to automatically deploy the machines is the right idea and Vagrant is here to help you.</p>

<h2 id="what-is-vagrant-basically-">What is Vagrant basically ?</h2>

<p>Let’s take an example with a Linux Operating System (OS) and KVM/libvirt as hypervisor. Let say that you simply want to test the new major version of your preferred OS and be sure that every tools you are using are still working. You probably won’t take the risk to completely erase your computer and install directly the OS. The best option could be to use a VM. You will then deploy the machine (via CLI with <code class="language-plaintext highlighter-rouge">virsh</code>/<code class="language-plaintext highlighter-rouge">virt-install</code> commands or via GUI with <em>Virtual Machine Manager</em> or <em>Cockpit</em>), eventuelly follow the installation process, reboot, install your tools and reconfigure your environment before (finally !) being able to test if everything is working as expected… Very long workflow that you have to reiterate for each major version release !</p>

<p>Another solution could be to describe once for all what you want to do and take this recipe each time you need it. This is basically what Vagrant is made for : you describe almost evrything in one file (called Vagrantfile) and you are done ! It is finally a simple abstraction layer above your hypervisor which takes over VM management and configuration.</p>

<p>We are talking about KVM hypervisor in our example but we could have choosen whichever one. Vagrant has a concept of <em>providers</em> which is basically a plugin that Vagrant can use to communicate with an hypervisor. VirtualBox is supported out of the box but you will have to install a custom provider to let Vagrant manage a KVM hypervisor via libvirt.</p>

<h2 id="first-step--install-vagrant">First step : install Vagrant</h2>

<p>Depending on your OS, there are multiple ways to install Vagrant. You can follow the <a href="https://developer.hashicorp.com/vagrant/docs/installation" target="_blank">official documentation</a> or an OS specific documentation. We will show how to do this with Fedora.</p>

<p>If libvirt is not already installed, there is a package group for that (called <em>Virtualization</em>). Vagrant and the libvirt provider is also shipped in another package group (called <em>Vagrant</em>). As a rusult, you can install both with :</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>dnf group <span class="nb">install </span>virtualization vagrant
</code></pre></div></div>

<p>You should then enable <code class="language-plaintext highlighter-rouge">livirtd</code> to automatically start and add your current user to <code class="language-plaintext highlighter-rouge">libvirt</code> group in order to avoid password prompting each time you start a VM. These can be done with the following commands :</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>systemctl <span class="nb">enable</span> <span class="nt">--now</span> libvirtd
<span class="nb">sudo </span>usermod <span class="nt">-aG</span> libvirt <span class="k">${</span><span class="nv">USER</span><span class="k">}</span>
newgrp libvirt
</code></pre></div></div>

<h2 id="quick-start--your-first-vagrantfile">Quick start : your first Vagrantfile</h2>

<p>As already explained, the power of Vagrant is to simply describe in a single file the whole infrastructure you want to create. The syntax uses Ruby language but don’t worry if you know nothing about this language, you basically just have to declare variables. Let’s have a look with these lines thant you can copy and past in a file named <code class="language-plaintext highlighter-rouge">Vagrantfile</code> :</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Vagrant</span><span class="p">.</span><span class="nf">configure</span><span class="p">(</span><span class="s2">"2"</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">config</span><span class="o">|</span>
    <span class="n">config</span><span class="p">.</span><span class="nf">vm</span><span class="p">.</span><span class="nf">box</span> <span class="o">=</span> <span class="s2">"fedora/41-cloud-base"</span>
<span class="k">end</span>
</code></pre></div></div>

<p>This Vagrantfile simply declares a VM which uses a Fedora 41 Cloud Base image. Juste note that OS images used by Vagrant are called <em>boxes</em> and are basically packages embedding metadata, configuration files and a VM image specifically configured for Vagrant. Hashicorp (the company behind Vagrant) hosts a <a href="https://portal.cloud.hashicorp.com/vagrant/discover" target="_blank">repository</a> with many boxes publicly available. As each box has a specific version for each provider (KVM/libvirt, VirtualBox, …), be sure that the box you want is compatbile with the provider(s) you want to use.</p>

<p>To launch the VM, the only thing you have to do is to run the following command in the directory where the Vagrantfile is :</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>vagrant up
</code></pre></div></div>

<p><strong>Note :</strong> For those already running Fedora, the Vagrant version shipped with the distribution uses <a href="https://libvirt.org/daemons.html#operating-modes" target="_blank">libvirt daemon <em>session mode</em></a>. As a result, Vagrant is running in an unprivileged mode and only has the same rights than the current user which is more safer. However, some functionalities (as network operations) require root permissions and Vagrant needs to run in <em>system mode</em>. This can be done by adding a libvirt option as described below :</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Vagrant</span><span class="p">.</span><span class="nf">configure</span><span class="p">(</span><span class="s2">"2"</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">config</span><span class="o">|</span>
    <span class="n">config</span><span class="p">.</span><span class="nf">vm</span><span class="p">.</span><span class="nf">box</span> <span class="o">=</span> <span class="s2">"fedora/41-cloud-base"</span>
  
    <span class="n">config</span><span class="p">.</span><span class="nf">vm</span><span class="p">.</span><span class="nf">provider</span> <span class="ss">:libvirt</span> <span class="k">do</span> <span class="o">|</span><span class="n">libvirt</span><span class="o">|</span>
        <span class="n">libvirt</span><span class="p">.</span><span class="nf">qemu_use_session</span> <span class="o">=</span> <span class="kp">false</span>
    <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<h2 id="provisioning-the-machine">Provisioning the machine</h2>

<p>Launching VM is interesting but in our example above we talked about making some tests in our new machine. Automatically do stuffs is what <em>provisioners</em> are made for. Once again, you can observe that Vagrant is very modular as there are many different provisioners available as detailed in <a href="https://developer.hashicorp.com/vagrant/docs/provisioning" target="_blank">Vagrant documentation</a>.</p>

<p>We are going to use Ansible provisioner to do stuffs in our VM. We won’t go too much into details on how Ansible works. The only important thing to know about Ansible is that it allows you to discribe in a file (called a <em>playbook</em>) a sequence of tasks that will be run inside the VM.</p>

<p>We first have to install Ansible on the host alongside Vagrant. On Fedora, this can be done with the following command :</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>dnf <span class="nb">install </span>ansible
</code></pre></div></div>

<p>Just copy and past in a file called <code class="language-plaintext highlighter-rouge">playbook.yml</code> the lines below. This file must be in the same directory as the Vagrantfile. This Ansible playbook simply updates the machine.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">---</span>
<span class="pi">-</span> <span class="na">hosts</span><span class="pi">:</span> <span class="s">all</span>
  <span class="na">gather_facts</span><span class="pi">:</span> <span class="no">true</span>
  <span class="na">tasks</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Upgrade all packages</span>
    <span class="na">ansible.builtin.dnf</span><span class="pi">:</span>
      <span class="na">name</span><span class="pi">:</span> <span class="s2">"</span><span class="s">*"</span>
      <span class="na">state</span><span class="pi">:</span> <span class="s">latest</span>
    <span class="na">register</span><span class="pi">:</span> <span class="s">update</span>
    <span class="na">become</span><span class="pi">:</span> <span class="no">true</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Reboot to apply updates</span>
    <span class="na">ansible.builtin.reboot</span><span class="pi">:</span>
      <span class="na">post_reboot_delay</span><span class="pi">:</span> <span class="m">60</span>
      <span class="na">reboot_command</span><span class="pi">:</span> <span class="s">reboot</span>
    <span class="na">when</span><span class="pi">:</span> <span class="s">update.changed</span>
    <span class="na">become</span><span class="pi">:</span> <span class="no">true</span>
</code></pre></div></div>

<p>You can then update you Vagrantfile to call the playbook once the VM is started for the <em>first time</em> :</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Vagrant</span><span class="p">.</span><span class="nf">configure</span><span class="p">(</span><span class="s2">"2"</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">config</span><span class="o">|</span>
    <span class="n">config</span><span class="p">.</span><span class="nf">vm</span><span class="p">.</span><span class="nf">box</span> <span class="o">=</span> <span class="s2">"fedora/41-cloud-base"</span>
  
    <span class="n">config</span><span class="p">.</span><span class="nf">vm</span><span class="p">.</span><span class="nf">provider</span> <span class="ss">:libvirt</span> <span class="k">do</span> <span class="o">|</span><span class="n">libvirt</span><span class="o">|</span>
        <span class="n">libvirt</span><span class="p">.</span><span class="nf">qemu_use_session</span> <span class="o">=</span> <span class="kp">false</span>
    <span class="k">end</span>

    <span class="n">config</span><span class="p">.</span><span class="nf">vm</span><span class="p">.</span><span class="nf">provision</span> <span class="s2">"ansible"</span> <span class="k">do</span> <span class="o">|</span><span class="n">ansible</span><span class="o">|</span>
        <span class="n">ansible</span><span class="p">.</span><span class="nf">playbook</span> <span class="o">=</span> <span class="s2">"playbook.yml"</span>
    <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>If it is not the first time you launch the VM, you can force provisioning with the following command :</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>vagrant up <span class="nt">--provision</span>
</code></pre></div></div>

<h2 id="multi-machine-infrastructure">Multi-machine infrastructure</h2>

<p>The examples above were only launching a single machine. Obviously, you can declare multiple machines that will be launched at the same time. The syntax is slightly different but the idea is the same : you just have to include each machine configuration variables inside a <code class="language-plaintext highlighter-rouge">config.vm.define</code> method.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Vagrant</span><span class="p">.</span><span class="nf">configure</span><span class="p">(</span><span class="s2">"2"</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">config</span><span class="o">|</span>
  
  <span class="n">config</span><span class="p">.</span><span class="nf">vm</span><span class="p">.</span><span class="nf">define</span> <span class="s2">"vm1"</span> <span class="k">do</span> <span class="o">|</span><span class="n">hostconfig</span><span class="o">|</span>
    <span class="n">hostconfig</span><span class="p">.</span><span class="nf">vm</span><span class="p">.</span><span class="nf">box</span> <span class="o">=</span> <span class="s2">"fedora/41-cloud-base"</span>

    <span class="n">hostconfig</span><span class="p">.</span><span class="nf">vm</span><span class="p">.</span><span class="nf">provider</span> <span class="ss">:libvirt</span> <span class="k">do</span> <span class="o">|</span><span class="n">libvirt</span><span class="o">|</span>
      <span class="n">libvirt</span><span class="p">.</span><span class="nf">qemu_use_session</span> <span class="o">=</span> <span class="kp">false</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="n">config</span><span class="p">.</span><span class="nf">vm</span><span class="p">.</span><span class="nf">define</span> <span class="s2">"vm2"</span> <span class="k">do</span> <span class="o">|</span><span class="n">hostconfig</span><span class="o">|</span>
    <span class="n">hostconfig</span><span class="p">.</span><span class="nf">vm</span><span class="p">.</span><span class="nf">box</span> <span class="o">=</span> <span class="s2">"fedora/41-cloud-base"</span>

    <span class="n">hostconfig</span><span class="p">.</span><span class="nf">vm</span><span class="p">.</span><span class="nf">provider</span> <span class="ss">:libvirt</span> <span class="k">do</span> <span class="o">|</span><span class="n">libvirt</span><span class="o">|</span>
      <span class="n">libvirt</span><span class="p">.</span><span class="nf">qemu_use_session</span> <span class="o">=</span> <span class="kp">false</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>You can even factor configuration variables that are the same accross machines. This way, the Vagrantfile just above would become :</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Vagrant</span><span class="p">.</span><span class="nf">configure</span><span class="p">(</span><span class="s2">"2"</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">config</span><span class="o">|</span>

  <span class="n">config</span><span class="p">.</span><span class="nf">vm</span><span class="p">.</span><span class="nf">box</span> <span class="o">=</span> <span class="s2">"fedora/41-cloud-base"</span>

  <span class="n">config</span><span class="p">.</span><span class="nf">vm</span><span class="p">.</span><span class="nf">provider</span> <span class="ss">:libvirt</span> <span class="k">do</span> <span class="o">|</span><span class="n">libvirt</span><span class="o">|</span>
    <span class="n">libvirt</span><span class="p">.</span><span class="nf">qemu_use_session</span> <span class="o">=</span> <span class="kp">false</span>
  <span class="k">end</span>
  
  <span class="n">config</span><span class="p">.</span><span class="nf">vm</span><span class="p">.</span><span class="nf">define</span> <span class="s2">"vm1"</span> <span class="k">do</span> <span class="o">|</span><span class="n">hostconfig</span><span class="o">|</span>
  <span class="k">end</span>

  <span class="n">config</span><span class="p">.</span><span class="nf">vm</span><span class="p">.</span><span class="nf">define</span> <span class="s2">"vm2"</span> <span class="k">do</span> <span class="o">|</span><span class="n">hostconfig</span><span class="o">|</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Naturaly, it is still possible to provision the VMs. In case we want to use the same playbook to provision each VM, the first approach would be to declare the playbook at <code class="language-plaintext highlighter-rouge">config</code> level :</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Vagrant</span><span class="p">.</span><span class="nf">configure</span><span class="p">(</span><span class="s2">"2"</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">config</span><span class="o">|</span>

  <span class="n">config</span><span class="p">.</span><span class="nf">vm</span><span class="p">.</span><span class="nf">box</span> <span class="o">=</span> <span class="s2">"fedora/41-cloud-base"</span>

  <span class="n">config</span><span class="p">.</span><span class="nf">vm</span><span class="p">.</span><span class="nf">provider</span> <span class="ss">:libvirt</span> <span class="k">do</span> <span class="o">|</span><span class="n">libvirt</span><span class="o">|</span>
    <span class="n">libvirt</span><span class="p">.</span><span class="nf">qemu_use_session</span> <span class="o">=</span> <span class="kp">false</span>
  <span class="k">end</span>

  <span class="c1"># Provisionning</span>
  <span class="n">config</span><span class="p">.</span><span class="nf">vm</span><span class="p">.</span><span class="nf">provision</span> <span class="s2">"ansible"</span> <span class="k">do</span> <span class="o">|</span><span class="n">ansible</span><span class="o">|</span>
    <span class="n">ansible</span><span class="p">.</span><span class="nf">playbook</span> <span class="o">=</span> <span class="s2">"playbook.yml"</span>
  <span class="k">end</span>
  
  <span class="n">config</span><span class="p">.</span><span class="nf">vm</span><span class="p">.</span><span class="nf">define</span> <span class="s2">"vm1"</span> <span class="k">do</span> <span class="o">|</span><span class="n">hostconfig</span><span class="o">|</span>
  <span class="k">end</span>

  <span class="n">config</span><span class="p">.</span><span class="nf">vm</span><span class="p">.</span><span class="nf">define</span> <span class="s2">"vm2"</span> <span class="k">do</span> <span class="o">|</span><span class="n">hostconfig</span><span class="o">|</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>This approach is not wrong but, as explained in the <a href="https://developer.hashicorp.com/vagrant/docs/provisioning/ansible#ansible-parallel-execution" target="_blank">Vagrant documentation</a>, Vagrant will provision VM in sequence so that two different playbooks will be launch. As the power of Ansible is to provision multiple VMs using parallelism (this means lauching a single playbook will provision as many VMs as you want), if you want to keep this behavior, you will have to modify a little bit the syntax. The Vagrantfile just above would thus become :</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Vagrant</span><span class="p">.</span><span class="nf">configure</span><span class="p">(</span><span class="s2">"2"</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">config</span><span class="o">|</span>

  <span class="n">config</span><span class="p">.</span><span class="nf">vm</span><span class="p">.</span><span class="nf">box</span> <span class="o">=</span> <span class="s2">"fedora/41-cloud-base"</span>

  <span class="n">config</span><span class="p">.</span><span class="nf">vm</span><span class="p">.</span><span class="nf">provider</span> <span class="ss">:libvirt</span> <span class="k">do</span> <span class="o">|</span><span class="n">libvirt</span><span class="o">|</span>
    <span class="n">libvirt</span><span class="p">.</span><span class="nf">qemu_use_session</span> <span class="o">=</span> <span class="kp">false</span>
  <span class="k">end</span>

  <span class="no">N</span> <span class="o">=</span> <span class="mi">2</span>
  <span class="p">(</span><span class="mi">1</span><span class="o">..</span><span class="no">N</span><span class="p">).</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">machine_id</span><span class="o">|</span>
    <span class="n">config</span><span class="p">.</span><span class="nf">vm</span><span class="p">.</span><span class="nf">define</span> <span class="s2">"vm</span><span class="si">#{</span><span class="n">machine_id</span><span class="si">}</span><span class="s2">"</span> <span class="k">do</span> <span class="o">|</span><span class="n">hostconfig</span><span class="o">|</span>

      <span class="c1"># Only execute once the Ansible provisioner,</span>
      <span class="c1"># when all the machines are up and ready.</span>
      <span class="k">if</span> <span class="n">machine_id</span> <span class="o">==</span> <span class="no">N</span>
        <span class="n">hostconfig</span><span class="p">.</span><span class="nf">vm</span><span class="p">.</span><span class="nf">provision</span> <span class="ss">:ansible</span> <span class="k">do</span> <span class="o">|</span><span class="n">ansible</span><span class="o">|</span>
          <span class="c1"># Disable default limit to connect to all the machines</span>
          <span class="n">ansible</span><span class="p">.</span><span class="nf">limit</span> <span class="o">=</span> <span class="s2">"all"</span>
          <span class="n">ansible</span><span class="p">.</span><span class="nf">playbook</span> <span class="o">=</span> <span class="s2">"playbook.yml"</span>
        <span class="k">end</span>
      <span class="k">end</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<h2 id="multi-provider-compatibility">Multi-provider compatibility</h2>

<p>Another good thing with Vagrant is that you can share your Vagrantfile with others. If you are developing stuffs and using Vagrant to make tests in one or multiple VMs, you can easily share your test infrastructure with other developers.</p>

<p>In case these others are using Vagrant with VirtualBox provider whereas you are using it with libvirt provider, it is still possible to make your Vagrantfile compatible with both. As described in the <a href="https://developer.hashicorp.com/vagrant/docs/providers/default" target="_blank">documentation</a>, Vagrant has a default provider that will be used to launch VMs. It is generally VirtualBox but it can be another one depending on the distribution (as Fedora is not providing a VirtualBox package, libvirt is the default provider).</p>

<p>As a result, the minimum Vagrantfile below should work exactly the same on different environments.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Vagrant</span><span class="p">.</span><span class="nf">configure</span><span class="p">(</span><span class="s2">"2"</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">config</span><span class="o">|</span>
    <span class="n">config</span><span class="p">.</span><span class="nf">vm</span><span class="p">.</span><span class="nf">box</span> <span class="o">=</span> <span class="s2">"fedora/41-cloud-base"</span>
<span class="k">end</span>
</code></pre></div></div>

<p>As a box is specific to a provider and an architecture, Vagrant will automatically get the box compatible with your own environment (if such a box exists of course).</p>

<p>However, it is sometimes necessary to explicitly write provider specific parameters. Some values are indeed managed by the provider and need to be re-written for all providers that will be used with the Vagrantfile. Let say that we discribe a VM with an amount of 8 Gb of memory and 4 CPUs. If this Vangrantfile will be used with VirtualBox and libvirt providers, it should then looks like :</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Vagrant</span><span class="p">.</span><span class="nf">configure</span><span class="p">(</span><span class="s2">"2"</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">config</span><span class="o">|</span>
    <span class="n">config</span><span class="p">.</span><span class="nf">vm</span><span class="p">.</span><span class="nf">box</span> <span class="o">=</span> <span class="s2">"fedora/41-cloud-base"</span>

    <span class="c1"># VirtualBox specific parameters</span>
    <span class="n">config</span><span class="p">.</span><span class="nf">vm</span><span class="p">.</span><span class="nf">provider</span> <span class="ss">:virtualbox</span> <span class="k">do</span> <span class="o">|</span><span class="n">vbox</span><span class="o">|</span>
      <span class="n">vbox</span><span class="p">.</span><span class="nf">memory</span> <span class="o">=</span> <span class="mi">8192</span>
      <span class="n">vbox</span><span class="p">.</span><span class="nf">cpus</span> <span class="o">=</span> <span class="mi">4</span>
    <span class="k">end</span>

    <span class="c1"># libvirt specific parameters</span>
    <span class="n">config</span><span class="p">.</span><span class="nf">vm</span><span class="p">.</span><span class="nf">provider</span> <span class="ss">:libvirt</span> <span class="k">do</span> <span class="o">|</span><span class="n">libvirt</span><span class="o">|</span>
      <span class="n">libvirt</span><span class="p">.</span><span class="nf">memory</span> <span class="o">=</span> <span class="mi">8192</span>
      <span class="n">libvirt</span><span class="p">.</span><span class="nf">cpus</span> <span class="o">=</span> <span class="mi">4</span>
    <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<h2 id="conclusion">Conclusion</h2>

<p>Vagrant is the perfect tool to easily describe, configure and eventually share a VM infrastructure. As shown in this post, you can quickly start to write a Vagrantfile. The Ruby syntax let you create more complicated stuffs. Don’t hesitate to also read Vagrant or associated plugins documentation to see the power of this tool.</p>

<p>Unfortunately, since v3.4.0, Vagrant does not have an <a href="https://en.wikipedia.org/wiki/Vagrant_(software)#License_change" target="_blank">open source license anymore</a> as the new <a href="https://en.wikipedia.org/wiki/Business_Source_License" target="_blank">Business Source License</a> restricts the use of the software to certain users. Such a change has impacts on how the product is integrated in open-source distribution like Fedora which only supports open-source softwares in their official repositories. The package is then distributed in version before v3.4.0 which does not allow user to easily get an updated version.</p>

<h2 id="sources">Sources</h2>
<ul>
  <li><a href="https://developer.hashicorp.com/vagrant/docs/installation" target="_blank">Vagrant official documentation</a></li>
  <li><a href="https://developer.hashicorp.com/vagrant/docs/installation" target="_blank">Vagrant documentation on Fedora Developer Portal</a></li>
  <li><a href="https://nts.strzibny.name/writing-a-general-multi-provider-vagrantfile/" target="_blank">Multi-provider Vagrantfile</a></li>
</ul>]]></content><author><name></name></author><category term="Virtualization" /><category term="vagrant" /><category term="libvirt" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">Verify Android application signature</title><link href="https://ohxodi.com/cryptography/2024/07/29/apk-signature-verification.html" rel="alternate" type="text/html" title="Verify Android application signature" /><published>2024-07-29T20:30:00+00:00</published><updated>2024-07-29T20:30:00+00:00</updated><id>https://ohxodi.com/cryptography/2024/07/29/apk-signature-verification</id><content type="html" xml:base="https://ohxodi.com/cryptography/2024/07/29/apk-signature-verification.html"><![CDATA[<p><img src="/assets/signature.jpg" alt="Signature" title="Signature" /></p>

<h2 id="introduction">Introduction</h2>

<p>The “official” way to get applications on your Android device is to use Google Play. You can easily find a large amount of apps and manage those you want to install or remove directly from the plateform.</p>

<p>There are however many reasons why you would prefer to avoid using Google Play and directly install apps on your device without using this plateform. An Android application is after all a package (<em>Android Package Kit</em> (APK) format) that can be installed directly on the device and Google Play is only a package manager (others exist on Android (e.g. <a href="https://f-droid.org/" target="_blank">F-Droid</a>)). But there are some risks to do so and this is why there are safeguards implemented by default on the operating system. Beyond those, there are also checks that can be directly done on the APK file you want to install in order to be sure that you are installing the one produced by the developer you trust. How to proceed to these verifications ? This what we are going to discuss in this article.</p>

<h2 id="default-safeguards">Default safeguards</h2>

<h3 id="install-permission">Install permission</h3>

<p>By default, Android doesn’t let you install a package that you have downloaded on the device or transfered from another device. As soon as you want to launch the app installation by selecting the APK file from the file manager, Android will complain that the file manager application is not allowed to install packages.</p>

<p>To bypass this safeguard, go to <code class="language-plaintext highlighter-rouge">Settings &gt; Apps</code>, select the file manager app and go to <code class="language-plaintext highlighter-rouge">Install unknown apps</code> to allow package installation. No need to keep this permission permanently, so you can remove it just after you have finished the package installation.</p>

<p>The same situation could occur when launching the APK file directly from your web browser. In this case, the latter should be allowed to install packages.</p>

<h3 id="signing-check">Signing check</h3>

<p>If you want to manually update an application that has already been installed on the device with Google Play for exemple, Android will probably complain, when launching the APK file, that the application is conflicting with an onther one on the device. In such a case, this is due to the signing check done by Android : both packages are signed with different keys and the operating system prevents the installation of a potentially malicious update that has not been built by an entity or a developer you trust.</p>

<p>However, there are many situations where there is no security problem. Indeed, it is simply possible that the package installed with a package manager like Google Play or F-Droid is signed with a different key than the one directly downloaded for a manual installation. As described in <a href="https://developer.android.com/studio/publish/app-signing#app-signing-google-play" target="_blank">Android documentation</a>, apps created after August 2021 must be signed with a key provided and managed by Google (app signing key). As a result, if a developer also want to provide its app outside Google Play pipeline, there is a significant chance that the APK will be signed with a different key.</p>

<p>In this situation, if you are sure that the APK file is signed by a trusted entity, the only way to bypass this safguard is to remove the previously installed package before installing manually the new one.</p>

<p>But how to manually verify a package signature ? This is in particular what we are going to discuss in the next section.</p>

<h2 id="signing-key-comparison">Signing key comparison</h2>

<p>The second safeguard described above only applies when a similar package is already installed. But when you want to manually install a new app on your device, the signature is a way to verify that the package has been built by a developer you trust. It reduces the risk that it contains milicious code which could be harmful for your device. Unfortunately, this verification needs some steps to be done.</p>

<p>Let say that you want to manually install Signal App on your smartphone without using Google Play. Even if it is risky and dedicated to “advanced user”, Signal let you <a href="https://signal.org/android/apk/" target="_blank">download the APK file directly</a> from their website and incite users to verify the SHA-256 fingerprint of the signing certificate. In other words, the safest way to manually install Signal is to compare the fingerprint of the signing certificate which belongs to the file you’ve downloaded with the one provided on Signal website. Doing so, if the verification succeeds, you can be 100% sure that the APK file has been built by Signal developers and can be safely installed on your device. (Except if the signing key has been stolen… Risk management is always a question of decision : at some point, you will have to trust enough the entity (organisation or developer) somehow.)</p>

<h3 id="get-apksigner-tool">Get <code class="language-plaintext highlighter-rouge">apksigner</code> tool</h3>

<p>This verification needs a tool called <code class="language-plaintext highlighter-rouge">apksigner</code> which is provided by the Android SDK build tools. You can download <a href="https://developer.android.com/studio#command-line-tools-only" target="_blank">Android Studio command line tools</a> to get it. As the tools are written with Java, a prerequisite is the installation of a Java JDK. For example, it can be easily installed with the command below :</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>dnf <span class="nb">install </span>java-latest-openjdk <span class="c"># Fedora</span>
<span class="nb">sudo </span>apt-get update <span class="o">&amp;&amp;</span> <span class="nb">sudo </span>apt-get <span class="nb">install </span>default-jdk <span class="c"># Debian</span>
</code></pre></div></div>

<p>If you have several Java versions installed on your system and if you need to switch to the latest, you can do so with the following command :</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>alternatives <span class="nt">--config</span> java <span class="c"># Fedora</span>
<span class="nb">sudo </span>update-alternatives <span class="nt">--config</span> javac <span class="c"># Debian</span>
</code></pre></div></div>

<p>Once the command line tools have been downloaded, you can verify the SHA-256 checksum of the zip file with the one provided on Android website before extracting the compressed files :</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sha256sum </span>commandlinetools-<span class="k">*</span>.zip
<span class="c"># If everything is OK :</span>
unzip commandlinetools-<span class="k">*</span>.zip
<span class="nb">cd </span>cmdline-tools
./bin/sdkmanager <span class="nt">--sdk_root</span><span class="o">=</span>./ <span class="s2">"build-tools;34.0.0"</span>
</code></pre></div></div>

<p>If the last command above returns with no error, you can then use <code class="language-plaintext highlighter-rouge">apksigner</code> tool.</p>

<h3 id="apk-signature-verification">APK signature verification</h3>

<p>From <code class="language-plaintext highlighter-rouge">cmdline-tools</code> folder, you can run <code class="language-plaintext highlighter-rouge">apksigner</code> with the following command where <code class="language-plaintext highlighter-rouge">&lt;PATH_TO_APK&gt;</code> is the path to the APK file :</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./build-tools/34.0.0/apksigner verify <span class="nt">--print-certs</span> &lt;PATH_TO_APK&gt; 
</code></pre></div></div>

<p>With Signal example, you should obtain the following result which is consistent with the information provided by Signal’s website.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./build-tools/34.0.0/apksigner verify <span class="nt">--print-certs</span> ../Signal<span class="k">*</span>.apk

Signer <span class="o">(</span><span class="nv">minSdkVersion</span><span class="o">=</span>33, <span class="nv">maxSdkVersion</span><span class="o">=</span>2147483647<span class="o">)</span> certificate DN: <span class="nv">CN</span><span class="o">=</span><span class="s2">"Signal Messenger, LLC"</span>, <span class="nv">SERIALNUMBER</span><span class="o">=</span>6703101, OID.2.5.4.15<span class="o">=</span>Private Organization, <span class="nv">O</span><span class="o">=</span><span class="s2">"Signal Messenger, LLC"</span>, OID.1.3.6.1.4.1.311.60.2.1.2<span class="o">=</span>Delaware, OID.1.3.6.1.4.1.311.60.2.1.3<span class="o">=</span>US, <span class="nv">L</span><span class="o">=</span>Mountain View, <span class="nv">ST</span><span class="o">=</span>California, <span class="nv">C</span><span class="o">=</span>US
Signer <span class="o">(</span><span class="nv">minSdkVersion</span><span class="o">=</span>33, <span class="nv">maxSdkVersion</span><span class="o">=</span>2147483647<span class="o">)</span> certificate SHA-256 digest: 4be4f6cd5be844083e900279dc822af65a547fecc26aba7ff1f5203a45518cd8
Signer <span class="o">(</span><span class="nv">minSdkVersion</span><span class="o">=</span>33, <span class="nv">maxSdkVersion</span><span class="o">=</span>2147483647<span class="o">)</span> certificate SHA-1 digest: 5c6740091301285db5409fdcd1b90f1ac3ba2dcf
Signer <span class="o">(</span><span class="nv">minSdkVersion</span><span class="o">=</span>33, <span class="nv">maxSdkVersion</span><span class="o">=</span>2147483647<span class="o">)</span> certificate MD5 digest: 34ac0b4e5d2c5c08b704fc05874f9a10
Signer <span class="o">(</span><span class="nv">minSdkVersion</span><span class="o">=</span>24, <span class="nv">maxSdkVersion</span><span class="o">=</span>32<span class="o">)</span> certificate DN: <span class="nv">CN</span><span class="o">=</span>Whisper Systems, <span class="nv">OU</span><span class="o">=</span>Research and Development, <span class="nv">O</span><span class="o">=</span>Whisper Systems, <span class="nv">L</span><span class="o">=</span>Pittsburgh, <span class="nv">ST</span><span class="o">=</span>PA, <span class="nv">C</span><span class="o">=</span>US
Signer <span class="o">(</span><span class="nv">minSdkVersion</span><span class="o">=</span>24, <span class="nv">maxSdkVersion</span><span class="o">=</span>32<span class="o">)</span> certificate SHA-256 digest: 29f34e5f27f211b424bc5bf9d67162c0eafba2da35af35c16416fc446276ba26
Signer <span class="o">(</span><span class="nv">minSdkVersion</span><span class="o">=</span>24, <span class="nv">maxSdkVersion</span><span class="o">=</span>32<span class="o">)</span> certificate SHA-1 digest: 45989dc9ad8728c2aa9a82fa55503e34a8879374
Signer <span class="o">(</span><span class="nv">minSdkVersion</span><span class="o">=</span>24, <span class="nv">maxSdkVersion</span><span class="o">=</span>32<span class="o">)</span> certificate MD5 digest: d90db364e32fa3a7bda4c290fb65e310
</code></pre></div></div>

<h3 id="source-of-truth">Source of truth</h3>

<p>Unfortunately, many developers don’t advertise the fingerprint of their signing certificate as explicitly as Signal. It is sometime possible to find it somewhere, like GitHub/GitLab issues or discussions on social networks.</p>

<p>If you cannot retrieve easily the information on Internet, an option is to get it from the package obtained via the “official” way (i.e. Google Play) <strong>if this one is signed with a developer key and not with one managed by Google as described above</strong> (only possible for apps created before August 2021).</p>

<p>In such a case, you can install first the package with Google Play, transfer the APK file on your computer and get the fingerprint with <code class="language-plaintext highlighter-rouge">apksigner</code>.</p>

<p>However, how to transfer the APK file from your device to your computer ? This can be done with <em>Android Debug Bridge</em> (ADB) that can be installed with the following command :</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>dnf <span class="nb">install </span>android-tools <span class="c"># Fedora</span>
<span class="nb">sudo </span>apt-get update <span class="o">&amp;&amp;</span> <span class="nb">sudo </span>apt-get <span class="nb">install </span>adb <span class="c"># Debian</span>
</code></pre></div></div>

<p>Before using ADB, you have to enable developer options and USB debugging on your device as described <a href="https://developer.android.com/studio/debug/dev-options#enable" target="_blank">here</a>.</p>

<p>You can then check that everything is working plugging in your Android device to your computer. A prompt on the device should ask you to trust the computer. Then the following command on the computer should show the connected device :</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>adb devices <span class="nt">-l</span>
</code></pre></div></div>

<p>The list of installed packages can be obtained with the following command :</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>adb shell pm list packages
</code></pre></div></div>

<p>If you have multiple users on your Android device, you can get the list of installed package for a specific user with this command where <code class="language-plaintext highlighter-rouge">&lt;USER_ID&gt;</code> is the ID of this user obtained with <code class="language-plaintext highlighter-rouge">adb shell pm list users</code> :</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>adb shell pm list packages <span class="nt">--user</span> &lt;USER_ID&gt;
</code></pre></div></div>

<p>The package names are displayed with their reversed Internet domain name which is a Java convention for package naming. Find the one you are looking for and get the path of the APK file on the device with this command where <code class="language-plaintext highlighter-rouge">&lt;APK_NAME&gt;</code> is the reversed Internet domain name of your package :</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>adb shell pm path &lt;APK_NAME&gt;
</code></pre></div></div>

<p>Finally, you can transfer your file on your computer with the following command where <code class="language-plaintext highlighter-rouge">&lt;REMOTE_APK_PATH&gt;</code> is the path previously obtained and <code class="language-plaintext highlighter-rouge">&lt;LOCAL_APK_PATH&gt;</code> is the path on your computer where you want to store the APK file :</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>adb pull &lt;REMOTE_APK_PATH&gt; &lt;LOCAL_APK_PATH&gt;
</code></pre></div></div>

<p>Once the APK file is on your computer, you can use <code class="language-plaintext highlighter-rouge">apksigner</code> to get the fingerprint and compare it with the one from the APK file manually downloaded. Once a package is manually installed, the second safeguard still applies and protects your device from installing a malicious package while updating the app.</p>

<h2 id="conclusion">Conclusion</h2>

<p>Signing packages is one of the most powerfull method to ensure that files provided are built from an entity you trust. This is exactly what is done for years with Linux packages and Android has just implemented similar mechanisms. We have seen that these checks are processed in the background as package managers like Google Play are abstracting everything.</p>

<p>By-passing packages managers is not a reason to decrease security level and this article has shown methods to check that a package is the right one, provided by an entity you trust, before installing it manually on your Android device.</p>

<h2 id="sources">Sources</h2>
<ul>
  <li><a href="https://www.privacyguides.org/en/android/obtaining-apps/#verifying-apk-fingerprints" target="_blank">Verifying APK Fingerprints</a></li>
  <li><a href="https://github.com/M66B/FairEmail?tab=readme-ov-file#downloads" target="_blank">Another way to verify APK Fingerprints</a></li>
  <li><a href="https://www.privacyguides.org/en/android/obtaining-apps/#verifying-apk-fingerprints" target="_blank">ADB Documentation</a></li>
</ul>]]></content><author><name></name></author><category term="Cryptography" /><category term="Android" /><category term="APK" /><category term="Signature" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">Mullvad VPN on Qubes OS</title><link href="https://ohxodi.com/network/2024/01/27/mullvad-vpn-qubes-os.html" rel="alternate" type="text/html" title="Mullvad VPN on Qubes OS" /><published>2024-01-27T22:08:14+00:00</published><updated>2024-01-27T22:08:14+00:00</updated><id>https://ohxodi.com/network/2024/01/27/mullvad-vpn-qubes-os</id><content type="html" xml:base="https://ohxodi.com/network/2024/01/27/mullvad-vpn-qubes-os.html"><![CDATA[<p><img src="/assets/tunnel.jpg" alt="Tunnel" title="Tunnel" /></p>

<h2 id="introduction">Introduction</h2>

<p>If you care about security and privacy, Qubes OS is a pretty good choice as it implements securely-isolated compartments called “Qubes” and integrates notably Whonix as a Tor gateway. More precisely, Qubes are virtual machines that takes advantage of all virtualization features to create those isolated compartments.</p>

<p>It is still possible to increase your privacy, espacially from your ISP, in using a VPN service like <a href="https://mullvad.net" target="_blank">Mullvad</a> within Qubes OS. There are plenty of VPN providers but <a href="https://www.privacyguides.org/en/vpn" target="_blank">Mullvad and some others</a> have made big efforts on transparency and are providing open-source clients (cf. <a href="https://mullvad.net/en/download/vpn" target="_blank">Mullvad VPN application</a>). Mullvad already provides guides like <a href="https://mullvad.net/en/help/wireguard-on-qubes-os/" target="_blank">this one</a> to help you configure the service on Qubes OS. It however only creates tunnel with a specifically choosen server and thus doesn’t use all the benefits of Mullvad VPN application like switching server, location or protocol.</p>

<p>We will explain how to install Mullvad VPN application in a similar configuration : a proxy Qube which provides to any other Qubes an Internet access through a Mullvad VPN.</p>

<p>The procedure bellow has been tested with <strong>Qubes OS 4.1</strong>.</p>

<h2 id="mullvad-vpn-application-installation-inside-a-proxy-qube">Mullvad VPN application installation inside a proxy Qube</h2>
<h3 id="create-a-new-template">Create a new template</h3>

<p>Qubes OS project’s security model give a great importance to base templates like Fedora or Debian. This is why any software should be installed from the default repositories of these distributions and, when it is not possible, the security of the template should be considered as downgraded.</p>

<p>As Mullvad VPN application is not in the default repositories, we will clone the Fedora template and use this new one to install the software as discribed in the <a href="https://www.qubes-os.org/doc/how-to-install-software/#installing-software-from-other-sources" target="_blank">Qubes OS documentation</a> :</p>

<ul>
  <li>Open the <code class="language-plaintext highlighter-rouge">Qube Manager</code> (<code class="language-plaintext highlighter-rouge">Qubes Tools</code> / <code class="language-plaintext highlighter-rouge">Qube Manager</code>)</li>
  <li>Click on the default Fedora template and open the <code class="language-plaintext highlighter-rouge">Settings</code> window</li>
  <li>You should be able to click on <code class="language-plaintext highlighter-rouge">Clone qube</code> button at the bottom right of the window</li>
  <li>Choose a non-black color for this template (red for example) indicating that it is potentially less secure that the default template</li>
  <li>Call your new template <code class="language-plaintext highlighter-rouge">fedora-XX-mullvad</code> where <code class="language-plaintext highlighter-rouge">XX</code> is the Fedora release number</li>
</ul>

<h3 id="give-internet-access-to-the-template">Give Internet access to the template</h3>

<p>This is the risky part of the process. Qubes OS project’s security model considers that templates are secured as it has no access to Internet and can only download packages from default repositories through a proxy. We will however have to give Internet access to our new template in order to download the necessary files :</p>

<ul>
  <li>In the <code class="language-plaintext highlighter-rouge">Qube Manager</code>, click on the <code class="language-plaintext highlighter-rouge">fedora-XX-mullvad</code> template and open the <code class="language-plaintext highlighter-rouge">Settings</code> window</li>
  <li>For <code class="language-plaintext highlighter-rouge">Net qube</code> option, select <code class="language-plaintext highlighter-rouge">sys-firewall</code></li>
  <li>In <code class="language-plaintext highlighter-rouge">Firewall rules</code> tab, select <code class="language-plaintext highlighter-rouge">Limit outgoing connections to ...</code> and click on <code class="language-plaintext highlighter-rouge">Allow full access for 15 min</code> in order to get an Internet access during 15 minutes (which should be enough to install Mullvad VPN application) and block it again when time has elapsed.</li>
</ul>

<p><strong>NOTE :</strong>
Adding and removing Internet access to the template is the prescribed way in Qubes OS documentation. You can however bypass these steps without added risks in reaching directly the Qube proxy :</p>
<ul>
  <li>Set <code class="language-plaintext highlighter-rouge">https_proxy</code> environment variable</li>
</ul>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="nb">export </span><span class="nv">https_proxy</span><span class="o">=</span>127.0.0.1:8082</code></pre></figure>

<ul>
  <li>Install Mullvad VPN App as explained below</li>
  <li>Once everything is installed, unset <code class="language-plaintext highlighter-rouge">https_proxy</code> environment variable</li>
</ul>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="nb">unset </span>https_proxy</code></pre></figure>

<h3 id="install-mullvad-vpn-application">Install Mullvad VPN application</h3>

<p>We will simply follow the Mullvad VPN application installation instructions on Fedora with a signature check :</p>

<ul>
  <li>Start the template <code class="language-plaintext highlighter-rouge">fedora-XX-mullvad</code></li>
  <li>Open a terminal for this template</li>
  <li>Download the signing key provided by Mullvad which should has been used to sign the RPM package :</li>
</ul>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash">curl https://mullvad.net/media/mullvad-code-signing.asc <span class="nt">-o</span> mullvad-code-signing.asc</code></pre></figure>

<ul>
  <li>Import the key and check that its fingerprint is <strong><code class="language-plaintext highlighter-rouge">A119 8702 FC3E 0A09 A9AE 5B75 D5A1 D4F2 66DE 8DDF</code></strong></li>
</ul>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash">gpg <span class="nt">--import</span> mullvad-code-signing.asc</code></pre></figure>

<p>(If you want to verify the fingerprint before importing the key, you can do so with <code class="language-plaintext highlighter-rouge">gpg --show-keys --with-fingerprint mullvad-code-signing.asc</code>)</p>
<ul>
  <li>Edit the key with the command <code class="language-plaintext highlighter-rouge">gpg --edit-key A1198702FC3E0A09A9AE5B75D5A1D4F266DE8DDF</code>. The output should be something like this :</li>
</ul>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash">gpg <span class="o">(</span>GnuPG<span class="o">)</span> 2.1.13<span class="p">;</span> Copyright <span class="o">(</span>C<span class="o">)</span> 2016 Free Software Foundation, Inc.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

pub  rsa4096/D5A1D4F266DE8DDF
     created: 2016-10-27  expires: never       usage: SC 
     trust: unknown       validity: unknown
sub  rsa4096/C187D22C089EF64E
     created: 2016-10-27  expires: never       usage: E  
sub  rsa4096/A26581F219C8314C
     created: 2016-10-27  expires: never       usage: S  
<span class="o">[</span> unknown] <span class="o">(</span>1<span class="o">)</span><span class="nb">.</span> Mullvad <span class="o">(</span>code signing<span class="o">)</span> &lt;admin@mullvad.net&gt;</code></pre></figure>

<ul>
  <li>If everything seems correct, you can trust the key via the gpg prompt : <code class="language-plaintext highlighter-rouge">gpg&gt; trust</code>. You will be asked to set the trust level. Set it to <code class="language-plaintext highlighter-rouge">5</code> in order to trust the key ultimately</li>
  <li>Leave the gpg prompt : <code class="language-plaintext highlighter-rouge">gpg&gt; q</code></li>
  <li>Download the app and its signature :</li>
</ul>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash">curl https://mullvad.net/download/app/rpm/latest <span class="nt">-o</span> mullvad.rpm
curl https://mullvad.net/download/app/rpm/latest/signature <span class="nt">-o</span> mullvad.asc</code></pre></figure>

<ul>
  <li>In the same folder where the files have been downloaded, run <code class="language-plaintext highlighter-rouge">gpg --verify mullvad.asc mullvad.rpm</code> to proceed to the signature check. You should see in the output of this command something like this :</li>
</ul>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash">gpg: Good signature from <span class="s2">"Mullvad (code signing) &lt;admin@mullvad.net&gt;"</span> <span class="o">[</span>ultimate]</code></pre></figure>

<ul>
  <li>If every verifications above have seceeded, you can then install the app with the folowing command :</li>
</ul>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="nb">sudo </span>dnf <span class="nb">install</span> <span class="nt">-y</span> ./mullvad.rpm</code></pre></figure>

<ul>
  <li>When the installation is finished, enable <code class="language-plaintext highlighter-rouge">mullvad-daemon</code> service so that the app launches automatically when the Qube starts :</li>
</ul>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="nb">sudo </span>systemctl <span class="nb">enable </span>mullvad-daemon</code></pre></figure>

<ul>
  <li>Finally shutdown the template</li>
</ul>

<h3 id="remove-internet-access-to-the-template">Remove Internet access to the template</h3>

<p>As we have given access to Internet to our new template, we will remove this one for consistency with Qubes OS project recommendations.</p>

<ul>
  <li>In the <code class="language-plaintext highlighter-rouge">Qube Manager</code>, click on the <code class="language-plaintext highlighter-rouge">fedora-XX-mullvad</code> template and open the <code class="language-plaintext highlighter-rouge">Settings</code> window</li>
  <li>For <code class="language-plaintext highlighter-rouge">Net qube</code> option, select back <code class="language-plaintext highlighter-rouge">default (none)</code></li>
</ul>

<h3 id="create-the-proxy-qube">Create the proxy Qube</h3>

<p>We will use our new template to create the proxy Qube which will privide an Internet access to any other Qube.</p>

<ul>
  <li>Open the <code class="language-plaintext highlighter-rouge">Qube Manager</code> (<code class="language-plaintext highlighter-rouge">Qubes Tools</code> / <code class="language-plaintext highlighter-rouge">Qube Manager</code>)</li>
  <li>Click on <code class="language-plaintext highlighter-rouge">New qube</code> to create a new Qube</li>
  <li>In <code class="language-plaintext highlighter-rouge">Basic</code> tab, you can call this Qube <code class="language-plaintext highlighter-rouge">sys-vpn</code> and use the color <code class="language-plaintext highlighter-rouge">red</code>. Select <code class="language-plaintext highlighter-rouge">AppVM</code> as Type, <code class="language-plaintext highlighter-rouge">fedora-XX-mullvad</code> as Template and <code class="language-plaintext highlighter-rouge">default (sys-firewall)</code> as Networking.</li>
  <li>In <code class="language-plaintext highlighter-rouge">Advanced</code> tab, enable <code class="language-plaintext highlighter-rouge">Provides network access to other qubes</code></li>
</ul>

<h3 id="configure-mullvad-vpn">Configure Mullvad VPN</h3>

<p>Everything should be ready to use Mullvad VPN. Start <code class="language-plaintext highlighter-rouge">sys-vpn</code> and use the following commands to configure your Mullvad account from CLI :</p>

<ul>
  <li>Configure your acount :</li>
</ul>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash">mullvad account login</code></pre></figure>

<p>You will be asked to enter your Mullvad account number.</p>
<ul>
  <li>Verify that your device is registered :</li>
</ul>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash">mullvad account get</code></pre></figure>

<p>You should see the device name that has been assigned by Mullvad to your Qube.</p>
<ul>
  <li>Configure the server you want to use (<em>e.g.</em> <code class="language-plaintext highlighter-rouge">se-mma-wg-001</code> which is a Wireguard server located in Sweden, in the city of Malmö) :</li>
</ul>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash">mullvad relay <span class="nb">set </span>location se-mma-wg-001</code></pre></figure>

<ul>
  <li>Connect to the server :</li>
</ul>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash">mullvad connect</code></pre></figure>

<ul>
  <li>Check that you are connected :</li>
</ul>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash">mullvad status</code></pre></figure>

<p>You should see something like <code class="language-plaintext highlighter-rouge">Connected to se-mma-wg-001 in Malmö, Sweden</code>.</p>
<ul>
  <li>Finally, auto-connect Mullvad VPN each time the Qube starts :</li>
</ul>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash">mullvad auto-connect <span class="nb">set </span>on</code></pre></figure>

<h3 id="fix-dns-problem">Fix DNS problem</h3>

<p>As Mullvad VPN application will change the DNS server in <code class="language-plaintext highlighter-rouge">/etc/resolv.conf</code>, it is necessary to run again the <code class="language-plaintext highlighter-rouge">/usr/lib/qubes/qubes-setup-dnat-to-ns</code> script which is normally executed when the Qube starts. It will update the DNAT rules in <code class="language-plaintext highlighter-rouge">iptables</code> so that to reach the right DNS server. You can verify the consistency of <code class="language-plaintext highlighter-rouge">/etc/resolv.conf</code> file and the DNAT rules with the command <code class="language-plaintext highlighter-rouge">sudo iptables -t nat -L</code>. This is a <strong>minimal workaround</strong> and this <a href="https://github.com/mullvad/mullvadvpn-app/issues/3803" target="_blank">DNS update should be directly managed by Mullvad VPN application</a>.</p>

<p>To do so, simply run the following command in <code class="language-plaintext highlighter-rouge">sys-vpn</code> Qube :</p>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="nb">sudo</span> /usr/lib/qubes/qubes-setup-dnat-to-ns</code></pre></figure>

<h3 id="configure-persistency-for-mullvad-vpn-application-configuration">Configure persistency for Mullvad VPN application configuration</h3>

<p>Most of data in <code class="language-plaintext highlighter-rouge">AppVM</code> are volatile, especially the ones in <code class="language-plaintext highlighter-rouge">/etc</code> folder and subfolders. Each <code class="language-plaintext highlighter-rouge">AppVM</code> Qube has a persistent folder in <code class="language-plaintext highlighter-rouge">/rw</code>. This is where you can keep configurations files across Qube reboots (<em>e.g.</em> in <code class="language-plaintext highlighter-rouge">/rw/config</code>). We will use this folder in order to be sure that configurations made in Mullvad VPN application are kept after each reboot. To do so, we will use the <code class="language-plaintext highlighter-rouge">/rw/config/rc.local</code> script which is executed everytime the Qube starts.</p>

<ul>
  <li>Create a persistent folder for Mullvad VPN application configuration files :</li>
</ul>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="nb">sudo mkdir</span> /rw/config/mullvad-vpn</code></pre></figure>

<ul>
  <li>Copy the already existing configuration files :</li>
</ul>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="nb">sudo cp</span> /etc/mullvad-vpn/<span class="k">*</span> /rw/config/mullvad-vpn/</code></pre></figure>

<ul>
  <li>Customise the <code class="language-plaintext highlighter-rouge">rc.local</code> file to reconfigure Mullvad VPN application at every startup. The file should looks like that :</li>
</ul>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="c">#!/bin/sh</span>

<span class="c"># Configure Mullvad VPN application</span>
<span class="nb">rm</span> <span class="nt">-rf</span> /etc/mullvad-vpn/
<span class="nb">ln</span> <span class="nt">-s</span> /rw/config/mullvad-vpn /etc/mullvad-vpn
systemctl restart mullvad-daemon

<span class="c"># fix DNS problem (https://github.com/mullvad/mullvadvpn-app/issues/3803)</span>
<span class="nb">sleep </span>10 <span class="c"># Waiting a bit that Mullvad succeeds to establish connection</span>
/usr/lib/qubes/qubes-setup-dnat-to-ns</code></pre></figure>

<h3 id="provide-internet-access-to-other-qubes-through-mullvad-vpn">Provide Internet access to other Qubes through Mullvad VPN</h3>

<p>We now have our proxy Qube called <code class="language-plaintext highlighter-rouge">sys-vpn</code> configured to build a tunnel to a Mullvad server using Mullvad VPN application. The last thing to do is to configure your Qubes to reach Internet through Mullvad VPN. For those Qubes, the only thing to do is to open the <code class="language-plaintext highlighter-rouge">Settings</code> and select <code class="language-plaintext highlighter-rouge">sys-vpn</code> for <code class="language-plaintext highlighter-rouge">Net qube</code> option instead of <code class="language-plaintext highlighter-rouge">default (sys-firewall)</code>.</p>

<p>You can verify that you are accessing Internet through a Mullvad VPN :</p>
<ul>
  <li>Open a terminal from the Qube connected to <code class="language-plaintext highlighter-rouge">sys-vpn</code></li>
  <li>Run the following command : <code class="language-plaintext highlighter-rouge">curl https://am.i.mullvad.net/connected</code>. The output should starts with <code class="language-plaintext highlighter-rouge">You are connected to Mullvad ...</code></li>
</ul>

<h3 id="optional-manage-configuration-via-mullvad-vpn-application-gui">(Optional) Manage configuration via Mullvad VPN application GUI</h3>

<p>With all the steps above, you should have a working configuration which let you access Internet through a Mullvad VPN. However we interacted with Mullvad VPN application <a href="https://mullvad.net/fr/help/how-use-mullvad-cli/" target="_blank">through the CLI</a>.</p>

<p>But you can also access to the GUI to interact with Mullvad VPN application. In <code class="language-plaintext highlighter-rouge">sys-vpn</code> qube settings, open <code class="language-plaintext highlighter-rouge">Applications</code> tab and add <code class="language-plaintext highlighter-rouge">Mullvad VPN</code> to the selected application.</p>

<p>You can then access the App GUI via <code class="language-plaintext highlighter-rouge">Service : sys-vpn</code> / <code class="language-plaintext highlighter-rouge">sys-vpn : Mullvad VPN</code>.</p>

<h2 id="conclusion">Conclusion</h2>

<p>We have set up a proxy VM which is able to provide to other Qubes an Internet access through a Mullvad VPN. This kind of configuration is based on what is done by default in Qubes OS with <code class="language-plaintext highlighter-rouge">sys-whonix</code> service which provides an Internet access through Tor to other Qubes.</p>

<p>As it has already been mentionned, the DNS update in <code class="language-plaintext highlighter-rouge">rc.local</code> file is a minimal workaround and has many ceveats. For example, if Mullvad VPN application connects to another server and changes the DNS, the DNAT rules won’t be updated automatically and the <code class="language-plaintext highlighter-rouge">/usr/lib/qubes/qubes-setup-dnat-to-ns</code> script should be run again manually. This is why the DNS issue should clearly be managed directly by Mullvad VPN application and the is the goal of this <a href="https://github.com/mullvad/mullvadvpn-app/issues/3803" target="_blank">Github issue</a>.</p>

<h2 id="sources">Sources</h2>
<ul>
  <li><a href="https://www.qubes-os.org/doc/how-to-install-software/" target="_blank">Qubes OS Documentation : How to install software</a></li>
  <li><a href="https://www.qubes-os.org/doc/networking/" target="_blank">Qubes OS Documentation : Networking</a></li>
  <li><a href="https://mullvad.net/en/help/wireguard-on-qubes-os/" target="_blank">Mullvad Documentation : Wireguard on Qubes OS</a></li>
  <li><a href="https://www.privacyguides.org/en/vpn" target="_blank">VPN providers respecting privacy</a></li>
</ul>]]></content><author><name></name></author><category term="Network" /><category term="VPN" /><category term="Wireguard" /><category term="Mullvad" /><category term="Qubes-OS" /><summary type="html"><![CDATA[]]></summary></entry></feed>