Advise on self-hosting backend on NixOS over tor

Context

while working to run self-privacy over tor, I am experiencing some difficulties deploying the selfprivacy api on a NixOs on virtualbox.
Initially I modified the linux app to recognise a mock graphql server that is served on a https localhost server (and https onion domain with self-signed certificates), however it is not the selfprivacy backend itself. Then I understood the app is not the actual backend, so tried to deploy the self-privacy api on a NixOs.

The digital ocean command seems to infect an ubuntu system with:

'name': hostName,
        'size': serverType,
        'image': 'ubuntu-24-04-x64',
        'user_data':
            '#cloud-config\n'
            'runcmd:\n'
            '- curl https://git.selfprivacy.org/SelfPrivacy/selfprivacy-nixos-infect/raw/branch/master/nixos-infect | '
            "API_TOKEN=$serverApiToken ENCODED_PASSWORD='$base64Password' "
            "DNS_PROVIDER_TOKEN=$dnsApiToken DNS_PROVIDER_TYPE=$dnsProviderType DOMAIN='$domainName' "
            "HOSTNAME=$hostName LUSER='${rootUser.login}' PROVIDER=$infectProviderName STAGING_ACME='$stagingAcme' "
            "${customSshKey != null ? "SSH_AUTHORIZED_KEY='$customSshKey'" : ""} "
            'bash 2>&1 | tee /root/nixos-infect.log',
        'region': region,
      };
      logger('Decoded data: $data');
      serverCreateResponse = await client.post('/droplets', data: data);
      dropletId = serverCreateResponse.data['droplet']['id'];

However, I thought it would be more direct to modify the NixOs backend/server directly to either deploy to localhost or to the chosen onion domain. In case localhost is chosen, a port-forward and/or torsocks/torrify may be used to map the local connections to the tor connections.

Assumptions

I assumed the following “deployment-hierarchy” is used by the self-privacy app:
SelfPrivacy/selfprivacy-nixos-template: A template for /etc/nixos contents (used by nixos-infect). Serves as a top-level NixOS configuration flake when deployed. - Forgejo: Beyond coding. We Forge. This uses the nixos-config repository (according to: his configuration is not self-contained in that config repo).
SelfPrivacy/selfprivacy-nixos-config: Immutable NixOS config - Forgejo: Beyond coding. We Forge. (Contains a configuration.nix wich is typically used in /etc/nixos/configuration.nix to configure the complete NixOS).
SelfPrivacy/selfprivacy-rest-api: SelfPrivacy GraphQL which allows app to control your server - Forgejo: Beyond coding. We Forge. I assume that is deployed by the selfprivacy-nixos-template and selfprivacy-nixos-config repositories respectively.

I do not exactly know what this is for:

This is used to convert an ubuntu droplet of digital ocean into a nixos system:

Approach

I tried to deploy the selfprivacy backend to a virtualbox NixOs system with:

# Backup hardware and original config to home dir
mkdir -p ~/backup
cp -r /etc/nixos ~/backup/
# Verify the backup generated the files:
ls ~/backup/nixos

cd /etc && sudo rm -r /etc/nixos

# overwrite the /etc/nixos dir with the selfprivacy-nixos-template repo content.
sudo git clone https://git.selfprivacy.org/SelfPrivacy/selfprivacy-nixos-template /etc/nixos
sudo chown -R swag:users /etc/nixos

 cd ~/backup/nixos

cp ~/backup/nixos/hardware-configuration.nix /etc/nixos/

# nix flake update # not needed no flakes are modified.
cd /etc/nixos
nixos-rebuild switch --flake /etc/nixos

However, that yields:

cd /etc/nixos
nixos-rebuild switch --flake /etc/nixos
warning: Git tree '/etc/nixos' is dirty
error: flake 'git+file:///etc/nixos' does not provide attribute 'packages.x86_64-linux.nixosConfigurations."nixos".config.system.build.nixos-rebuild', 'legacyPackages.x86_64-linux.nixosConfigurations."nixos".config.system.build.nixos-rebuild' or 'nixosConfigurations."nixos".config.system.build.nixos-rebuild'

So I retried with:

 sudo nixos-rebuild switch --flake .#default

which yields:

warning: Git tree '/etc/nixos' is dirty
building the system configuration...
warning: Git tree '/etc/nixos' is dirty
evaluation warning: system.stateVersion is hardcoded
error:
       … while calling the 'head' builtin
         at /nix/store/j95fcik6rzsydwips4m89dmlvfj9hg9y-source/lib/attrsets.nix:1534:13:
         1533|           if length values == 1 || pred here (elemAt values 1) (head values) then
         1534|             head values
             |             ^
         1535|           else

       … while evaluating the attribute 'value'
         at /nix/store/j95fcik6rzsydwips4m89dmlvfj9hg9y-source/lib/modules.nix:1084:7:
         1083|     // {
         1084|       value = addErrorContext "while evaluating the option `${showOption loc}':" value;
             |       ^
         1085|       inherit (res.defsFinal') highestPrio;

       … while evaluating the option `system.build.toplevel':

       … while evaluating definitions from `/nix/store/j95fcik6rzsydwips4m89dmlvfj9hg9y-source/nixos/modules/system/activation/top-level.nix':

       (stack trace truncated; use '--show-trace' to show the full, detailed trace)

       error:
       Failed assertions:
       - You must set the option ‘boot.loader.grub.devices’ or 'boot.loader.grub.mirroredBoots' to make the system bootable.

Even though I included the original hardware-configuration.nix that was created by the NixOS VM in virtualbox.

  1. Determine what to put in boot of /etc/nixos/hardware-configuration.nix:
ls /sys/firmware
# Yields:
# acpi devicetree dmi memmap

lsblk -f
# Yields:
#NAME   FSTYPE FSVER LABEL UUID                                 FSAVAIL FSUSE% MOUNTPOINTS
#sda                                                                           
#└─sda1 ext4   1.0         7e104c63-4749-4457-8487-052c9ecda33e   20,2G    26% /nix/store
                                                                              /

which appearently implies for me it does not use efi, and I should add this in the /etc/nixos/hardware-configuration.nix:

  boot.loader.grub.enable = true;
  boot.loader.grub.device = "/dev/sda";
  boot.loader.grub.efiSupport = false;
  boot.loader.grub.useOSProber = true;
  boot.loader.efi.canTouchEfiVariables = false;
  1. Update the userdata.json:

4.a Generate the password hash:

pip install passlib

generate a gen_pwd0.py file with content:

from passlib.hash import sha512_crypt

password = "your_example_password"
hashed = sha512_crypt.hash(password)
print(hashed)  # always starts with $6$

Then run the py file to generate the password hash:

python gen_pwd0.py

4.b Modify the userdata.json (and store that password hash in there and some onion domain):

{
    "dns": {
        "provider": "CLOUDFLARE",
        "useStagingACME": false
    },
    "server": {
        "provider": "localhost"
    },
    "domain": "some_onion_domain.onion",
    "hashedMasterPassword": "$6$rounds=656000$LJzzRV0Zmot3tXiU$some_long_hash/rl.89xnknslqgkYMUHlmXbflzBbTjQQ6BNkZ2kY/LaP3suR3QC79gM6fth9i1",
    "hostname": "hostnameplaceholder",
    "timezone": "Europe/Uzhgorod",
    "username": "placeholder",
    "useBinds": true,
    "sshKeys": [],
    "users": [],
    "autoUpgrade": {
        "enable": true
    },
    "workarounds": {
        "deleteNextcloudAdmin": true
    },
    "postgresql": {
        "location": "/var/lib/postgresql"
    },
    "modules": {
        "bitwarden": {
            "enable": false,
            "location": "SDA",
            "subdomain": "password"
        },
        "gitea": {
            "enableSso": true,
            "enable": false,
            "location": "SDA",
            "subdomain": "git"
        },
        "nextcloud": {
            "enableSso": true,
            "enable": true,
            "location": "SDA",
            "subdomain": "cloud"
        },
        "roundcube": {
            "enable": false,
            "enableSso": true
        },
        "simple-nixos-mailserver": {
            "enable": true,
            "location": "SDA"
        },
        "monitoring": {
            "enable": true,
            "location": "SDA"
        }
    }
}

4.c Copy userdata1.json into /etc/nixos/userdata.json

4.d Reconfigure the nixos with:

rm flake.lock
nix flake update --extra-experimental-features 'nix-command flakes'
sudo nixos-rebuild switch --flake .#default

Then does not fail, but some how ends up with a black screen and 15min+ blinking cursor. After a forced reboot, I get into the shell and can login with the password from the python file, so it at least applied the package.

Errors

To inspect if it worked/how it failed:

systemctl list-units --type=service --state=running

yields:

systemctl status nginx
systemctl status postfix-setup
systemctl status acme-secrets
systemctl status nextcloud-post-setup

failed.

Question

  1. How would you advice I approach running selfprivacy over tor?
  2. would it be better to modify the infect-strategy instead?
    If it perhaps is effective to directly modify the selfprivacy-nixos-template (and userdata.json);
  3. do you have any pointers on how I could proceed with the modifications (e.g. the dns provider is not needed for the tor setup)?
1 Like

Oof. I guess you got us nerd-sniped.

There are a lot of moving parts here that were deployed incorrectly. We will reply with a full breakdown of what can be done a little later, it will take some time to document this.

Thank you for your quick and clear response!

To prevent double work; I am aware some services, like Nextcloud, may require SSL to function. Generating self-signed SSL certificates for Onion domains have been automated here (and refactored into modular bash codes here). That setup is usable to self-host Nextcloud with a https://some_onion.onion domain over SSL by adding the self-signed certificated to the trusted store of Ubuntu and/or Android. I assume I will be able to convert that functionality into Nix, so my curiosity mainly goes out to how I could modify the Self-privacy components to work with onion domains, not so much on building Tor/SSL certs etc.

It seems not unlikely to me that some services may practically not work well over Tor. Specifically, I’ve understood email is quite a world on its own w.r.t. spam etc. and perhaps using tor somewhere in the chain does not make that easier. A partial functionality would be acceptable for me for the time being.

My primary objective is to build some basic support/MWE that I can run and use self-sufficient with Tor, so that I can encounter some papercuts, fix them over time, and eventually potentially discuss whether a PR may be feasible, such that more/other people can benefit from the labor/functionality as well.

1 Like

Any update of this? Seems nothing changes so far, i tried to use NixOS LXC Container

Thank you for your question and for sharing an alternative containerization/isolation method. I currently do not have anything new to share on this topic, and am curious about any suggestions that may be provided on how to approach this effectively.

1 Like

AFAIK Nextcloud doesn’t require SSL (i don’t know where you read this)

Thank you for your clarification. I assume you are right.

I encountered that issue 2 or 3 years ago when I included Collabora Online as a Nextcloud module, or when I synced the Nextcloud caldendar with davdroid over tor. I am currently fuzzy as to where exactly that requirement came from.

However, since I assume there are more services that will not provide full functionality if the url is not httpS, and providing an httpS:// onion domain is already practically automated except for the certificate sharing on the other devices, I would prefer to include httpS by default (over http). I would be happy to build a http-only variant if time permits, however, allocate lower priority to that.

On a side note, here is my intermediate progress, I did not yet verify the code, however I expect it runs the SelfPrivacy backend in NixOS in virtualbox on a Ubuntu host over an https onion domain automatically, and I’m currently trying to make the flutter app connect to that SelfPrivacy Backend with "I already have a server → → enter recovery key. That last step still seems to hang. The android app does not yet recognise the backend on the onion domain (when used in combination with orbot). If I get a working setup I will share it here.

Can you open api[.]blablabla[.]onion? According to my research app uses this subdomain for communication

Yes! The GraphQL API version is {“version”:“3.7.4”};


Furthermore, I’m able to automatically retrieve the onion domain and a recovery key from the SelfPrivacy NixOS backend deployed to virtualbox on the host, with ./get-recovery-key.sh and enter that into the modified flutter app on the Ubuntu host to go into actual app*:

Disclaimer

*However, it is still far from functional. For example; this commit skips building a factory because for onion domains the object cannot (yet) be filled with the data used in/for Hetzner/Digital Ocean. I expect that that is a non-sustainable shortcut, based on what I recall from a few months earlier where I believe to have read/seen that the factory is re-used (and hence) needed in a lot of places. Also, I did not test a single service yet (because of the above issue, the factory not existing).

I will continue development, hopefully tomorrow. I considered it valuable to take a few steps back from the previous setup, which already reached this point of functionality, to improve structure and reproducibility, I hope anyone can reproduce the above screenshots based on the repo, and perhaps beat me to the punch.

A brief update inbetween. Currently the SelfPrivacy NixOS backend seems to deploy reliably to http, not https, and the Ubuntu flutter app is able to connect to the SelfPrivacy backend over the onion domain and display services like Nextcloud in the flutter app (for configuration). In the browser on the host I can log in to Nextcloud over the onion domain*. That is in integrate-services branch.

*However, early in the development, the login method I forgot what it was called, not Keycloak but Kanidm, was evaded/not used, I believe because it was difficult with https over tor. Subsequently, http was used instead of https. Furthermore the development did not take into account the end objective of providing a PR back into SelfPrivacy that leaves the nominal/normal functionality with DNS etc. completely in tact, so I expect some nominal functions to be destroyed by making .onions work. So once proper functionality of NixOS, Flutter Ubuntu app and Android App with a few services is realised, I intend to do a rebuild to that objective with httpS and full compatibility with normal domains/normal functionality.

Development speed (and eventually quality) can be greatly improved with more Claude usage, in case anyone would like to provide that for solely this task, I hope you feel free to reach out.

The VM reliably hosts the SelfPrivacy NixOS backend over tor with https.

HTTPS appears to be working for the selfprivacy app on the Ubuntu host:
selfprivacy-app-tor-demo

And for the Android the browser https Nextcloud login appears to be working. Below is a CLI gif of the emulated Nextcloud on Android, however, in practice it appears Nextcloud login via the browser does work but on the Nextcloud app over https it does not yet work.
nextcloud-app-tor-demo

The Nextcloud app on Android is not yet able to login over https, I expect because the login requires one to login via the a browser with Tor (and I expect the feedback from browser back to app does not yet work). On the Ubuntu host I noticed the Nextcloud App did not receive a message back from a successful browser https authentication/login with tor (when it came from the Nextcloud app)..

Furthermore, I am working to automate the android app testing, with some form of Android emulation, such that A CI/CD pipeline can be set up, and to facilitate automated debugging with a visual and improve the reproducabliity. This proves challenging as Orbot does not yet seem to play happy with the Android emulator/emulation.

The points above regarding not breaking the normal/nominal SelfPrivacy functionality still need addressing, and I may still allow generalization to alternative decentralized networks. However, my primary focus is still on first making it work properly and reliably on Ubuntu and Android.

Wonder such developments to be included in main app/service but as developer said