Momo Cloud
Momo cloud is a cloud solution for individuals and small organizations.
Our focus is to provide the following benefits:
- Single-Sign-On everywhere
- Integrated services
- Automated & tested backups
- Automatic https on all services
- Federated services
- Full disk encryption
- Reproducibility: take your config to a new machine and deploy it, then restore a backup to have a 1-1 clone.
Services & Features
Communication
| Feature | Service used |
|---|---|
| Contacts | Nextcloud |
| Dovecot & Postfix | |
| Chat | Matrix |
| Video Calling | Nextcloud talk |
| Mailing lists | Listmonk |
Files
| Feature | Service used |
|---|---|
| File Server & Sync | Nextcloud |
| Document management | Paperless |
Project management & tasks
| Feature | Service used |
|---|---|
| Calendar | Nextcloud |
| Project management | OpenProject |
| Ticketing tool | Zammad |
| Timetracking | SolidTime |
Finances
| Feature | Service used |
|---|---|
| Invoicing | InvoicePlane |
Knowledge management
| Feature | Service used |
|---|---|
| Wiki | MediaWiki |
| Project Management | OpenProject |
Software development
| Feature | Service used |
|---|---|
| Git server | Forgejo |
Integrations
Integrations that currently work are the following:
- All services can use the built-in E-Mail server
- OpenProject can use the nextcloud file storage
momo-cloud documentation
This directory holds a collection of notes and documentation for momo-cloud usage.
Backups with restic
Paperless
Nighly backups can be enabled by using the option momo-cloud.backups.repos.${repo}.
To restore a paperless backup, one option is to use the following commands, which use document_exporter and document_importer paperless manage.py CLI subcommands under the hood.
Before starting the restore, stop the paperless process:
sudo mkdir -p /var/lib/restore/paperless
sudo restic-paperless-${repo} restore latest --tag paperless --target /var/lib/restore/paperless
paperless-restore /var/lib/restore/paperless
Modules
Nextcloud Whiteboard Server
Create & collaborate on an infinite canvas! https://github.com/nextcloud/whiteboard
Usage
momo-cloud.nextcloud-whiteboard = {
enable = true;
secretFile = config.age.secrets."nextcloud-whiteboard-server-secrets".path;
};
momo-cloud.nextcloud.whiteboard = {
enable = true;
jwtSecretFile = config.age.secrets."nextcloud-whiteboard-jwt-secret".path;
};
Secrets
secretFile
Generate the JWT secret, used by services.nextcloud-whiteboard-server.secretFiles:
openssl rand -hex 24
This secrets needs to be in the format expected by systemd’s EnvironmentFile directory, for example:
JWT_SECRET_KEY=<secret-string>
jwtSecretFile
Should contain the same string as secretFile, in this format:
<secret-string>
Used to automatically configure the Nextcloud whiteboard app.
LimeSurvey
Open source survey application. https://www.limesurvey.org
Prerequisites
Limesurvey does not play nice with postgres. The database connection via a socket is not supported, and as a result the nixpkgs module is also halfbroken with postgres.
This module does not use our usual single sign-on process via OIDC. Support via a plugin is possible, but still needs to be implemented.
There are currently no working tests for this module.
As a workaround Momo Cloud vendors its own limesurvey module, which creates the
postgres user together with a password allowing for TCP connections. This opens
up the possibility of a createLocal.
Limesurvey uses the yii php framework, currently in Version 1.1 which is EOL at the end of 2026. There appear to not be any public upgrade plans.
Usage
momo-cloud.limesurvey = {
enable = true;
databasePasswordFile = config.age.secrets."limesurvey-database-password".path;
encryptionKeyFile = config.age.secrets."limesurvey-encryption-key".path;
encryptionNonceFile = config.age.secrets."limesurvey-encryption-nonce".path;
oidcSecretFile = config.age.secrets."limesurvey-oidc-secret".path;
};
Secrets
To generate the encryption key for encryptionKeyFile, you can use xxd:
xxd -u -l 32 -p /dev/urandom
The encryption nonce for encryptionNonceFile works the same, but with 24 bytes:
xxd -u -l 24 -p /dev/urandom
To generate the OIDC secret for oidcSecretFile, used by services.kanidm.provision, you can use xxd:
xxd -u -l 32 -p /dev/urandom
To generate the postgres database password for databasePasswordFile, you can use xxd:
xxd -u -l 32 -p /dev/urandom
After setup, the admin user account will have the password password. This needs to be changed!
OIDC
An OIDC Plugin is shipped with the momo cloud LimeSurvey module, however it is not enabled by default.
The reason for this is that automatic user and group creation does not exist. You will have to log in to an existing admin user and create user accounts manually. These will then be able to log in via OIDC.
Plugins
Plugins can be added as simple zip file packages. They are available for installation in LimeSurvey, however they are not automatically installed or activated since LimeSurvey does not provide an easy API or CLI to do this.
Kanidm
Self-hosted lightweight identity management service. https://kanidm.github.io
Two groups are created by default:
admins: this will contain all admin groups for enabled applications (e.g.forgejo_admins)users: this will contain all user groups for enabled applications (e.g.forgejo_users)
Usage
momo-cloud.authentication = {
enable = true;
adminPasswordFile = config.age.secrets."kanidm-admin-password".path;
idmAdminPasswordFile = config.age.secrets."kanidm-idm-admin-password".path;
mailPasswordFile = config.age.secrets."kanidm-mail-password".path;
};
Secrets
adminPasswordFile
Generate the password:
openssl rand -hex 24
idmAdminPasswordFile
Generate the password:
openssl rand -hex 24
mailPasswordFile
Should be the password for the SMTP mail account.
User management with kanidm
Example ~/.config/kanidm:
["staging"]
uri = "https://account.staging.example.com"
["prod"]
uri = "https://account.example.com"
Initial login:
KANIDM_INSTANCE=staging kanidm login -D idm_admin
Creating users:
KANIDM_INSTANCE=staging kanidm person create $username $displayName
Updating a users email address:
KANIDM_INSTANCE=staging kanidm person update $username --mail "initial@email.address"
Adding users to groups:
KANIDM_INSTANCE=staging kanidm group add-members paperless_users $username
Generating a link to be sent to user for credentials creation:
KANIDM_INSTANCE=staging kanidm person credential create-reset-token $username
Change login timeout, for example to 3 months:
KANIDM_INSTANCE=staging kanidm group account-policy auth-expiry idm_all_accounts 7776000
Update
Pre-update check:
Example:
sudo -u kanidm /nix/store/wwlis495qq380mvwkccw23ixgg92dz61-kanidm-1.6.4/bin/kanidmd domain upgrade-check -c /nix/store/w5bipks8vzzs83xcx0sj8wi5n791lskc-server.toml
📜 Using config file: "/nix/store/w5bipks8vzzs83xcx0sj8wi5n791lskc-server.toml"
00000000-0000-0000-0000-000000000000 INFO i [info]: Starting Kanidmd | version: 1.6.4
00000000-0000-0000-0000-000000000000 WARN 🚧 [warn]: WARNING: /nix/store/w5bipks8vzzs83xcx0sj8wi5n791lskc-server.toml has 'everyone' permission bits in the mode. This could be a security risk ...
00000000-0000-0000-0000-000000000000 INFO i [info]: Running domain upgrade check ...
00000000-0000-0000-0000-000000000000 INFO i [info]: domain_name : account.staging.momo.koeln
00000000-0000-0000-0000-000000000000 INFO i [info]: domain_uuid : f7496e7b-a183-4cc7-bd14-6eb02ca1d288
00000000-0000-0000-0000-000000000000 INFO i [info]: domain_current_level : 9
00000000-0000-0000-0000-000000000000 INFO i [info]: domain_upgrade_level : 10
Logging pipeline completed shutdown
Monitoring server
Loki logs retention
We keep logs in loki for two weeks, configured by retention_period.
Listmonk
Open source mailing list application. https://listmonk.app
Prerequisites
By default, listmonk does not create an admin user, instead offering a setup page on initial startup. This should be prevented by creating an initial admin user with environment variables. See the secretFile option.
Mutable vs. Immutable settings
Even though a lot of settings are provisioned with nix, they are written to the database and editable via the listmonk UI.
Some settings (like the OIDC settings and root URL) will be overwritten every time the application gets redeployed. Other settings will only be written once during initial install (like the SMTP mailbox).
Usage
momo-cloud.listmonk = {
enable = true;
oidcSecretFile = config.age.secrets."listmonk-oidc-secret".path;
secretFile = config.age.secrets."listmonk-secret-file".path;
};
Secrets
To generate the OIDC secret for oidcSecretFile, used by services.kanidm.provision, you can use xxd:
xxd -u -l 32 -p /dev/urandom
The secret file is a file that will be handed to the listmonk service as a systemd EnvironmentFile. There are two variables that should always be set:
LISTMONK_ADMIN_USER: The admin user nameLISTMONK_ADMIN_PASSWORDThe admin user password
To generate the admin password:
xxd -u -l 32 -p /dev/urandom
Immich
Open source Photo organizing app with local AI. https://immich.app
Prerequisites
By default, immich does not create an admin user, instead offering a setup page on initial startup. This should be prevented by creating an initial admin user with environment variables. See the adminPasswordFile option.
Usage
momo-cloud.immich = {
enable = true;
oidcSecretFile = config.age.secrets."immich-oidc-secret".path;
adminPasswordFile = config.age.secrets."immich-admin-password".path;
};
Secrets
To generate the OIDC secret for oidcSecretFile, used by services.kanidm.provision, you can use xxd:
xxd -u -l 32 -p /dev/urandom
The admin password file holds the pasword for the initial admin user that gets created during the setup. To generate:
xxd -u -l 32 -p /dev/urandom
Vaultwarden
Open source password manager. https://github.com/dani-garcia/vaultwarden
Prerequisites
Vaultwarden needs a properly configured SMTP server to complete a user registration.
Usage
momo-cloud.vaultwarden = {
enable = true;
oidcSecretFile = config.age.secrets."vaultwarden-oidc-secret".path;
envFile = config.age.secrets."vaultwarden-secrets-env".path;
};
The secrets in the env file that need to be set are SSO_CLIENT_SECRET and SMTP_PASSWORD.
Example:
SSO_CLIENT_SECRET="secret"
SMTP_PASSWORD="password"
There are two kanidm groups for using vaultwarden:
vaultwarden_admins: grants admin rights (not implemented)vaultwarden_users: allows users to access the application
Secrets
To generate the OIDC secret for oidcSecretFile, used by services.kanidm.provision, you can use xxd:
xxd -u -l 32 -p /dev/urandom
Zammad
Open source Helpdesk and Support tool. https://zammad.com
Prerequisites
By default, zammad is not connected to OIDC. This has to be done manually with the admin account.
Usage
momo-cloud.zammad = {
enable = true;
secretKeyBaseFile = config.age.secrets."zammad-secret-base-file".path;
};
Secrets
To generate the secret for secretKeyBaseFile, you can use openssl:
openssl rand -hex 64
Moving to a different Domain
If the TLD is changed, there are two manual changes that need to change in the zammad config. You can perform these either with the admin account or via the rails console after you’ve deployed the instance with the new domain:
To start the rails console, log in via ssh and run the following:
sudo zammad run rails c
- Update the fqdn: see the zammad docs
- Update the OIDC config
For the second part, you’ll have to update the kanidm issue URL.
First, get the current config from the database:
c = Setting.get('auth_openid_connect_credentials')
=> {"display_name" => "Teylen Cloud ID",
"identifier" => "zammad",
"issuer" => "https://account.OLD_DOMAIN/oauth2/openid/zammad",
"uid_field" => "",
"scope" => "",
"pkce" => true}
Then, update the issuer string:
c[:issuer] = "https://account.NEW_DOMAIN/oauth2/openid/zammad"
=> "https://account.NEW_DOMAIN/oauth2/openid/zammad"
Last, write this out to the database:
Setting.set('auth_openid_connect_credentials', c)
Openproject
Project management software. https://openproject.com
Usage
momo-cloud.openproject = {
enable = true;
keyBaseFile = config.age.secrets."openproject-keybase".path;
oidcSecretFile = config.age.secrets."openproject-oidc-secret".path;
environmentFile = config.age.secrets."openproject-env-secrets".path;
extraSeedEnvironmentFile = config.age.secrets."openproject-seeder-secrets".path;
};
Secrets
keyBaseFile
Generate the keybase:
openssl rand -hex 32
oidcSecretFile
Generate the OIDC secret, used by services.kanidm.provision:
openssl rand -hex 48
environmentFile
The environmentFile for secrets env should at least contain the SMTP password:
OPENPROJECT_SMTP__PASSWORD="smtp-password"
extraSeedEnvironmentFile
The seeder secrets env looks like this:
OPENPROJECT_OPENID__CONNECT_KANIDM_SECRET="oidc-secret"
OPENPROJECT_SEED__ENTERPRISE__TOKEN='-----BEGIN OPENPROJECT-EE TOKEN-----
eyJkYXRhIjoiLzhGUUJkUmhMbHN6K0JKS2htVVJyTUUrQ2lDSXpsdEs0TzY2
UTFXVDEwUUxXVU1KYm5Fd1p4bkJ0NW1HLzBta093aWR1UVBIN2FtV0pzNERP
WmxwSDQycjBhbDAxM2pYSnBuOGxwL2E3ZUNIc3NCenk1Q1hESzk2VmduUkhS
d1oybllzaVMrSnA2QnE2Zk8xYThFbld5NGN6ZXR2VThaL3JEMzdDVEpuMnhZ
QXhvbzFaZ1VNcmVMSERlSzZnRU9qWFBSQStHWWVKcEtqQ0hnWGxmSzcrQ1ZT
S0p0RUxnUTRTRVI2aVZyVnlPREIwdkZSUVZjdGYrc2IxL2NwcTVqRjBsdURF
WVpmRVZTLzlkbDA5UVAvY3dobnhZUDBOVUt6Wi9CZ1pNUHVHbG52aVA0SHBa
RW1ockZXcUpyYnIvckVPQzdqZXpkSkQxdGw5Y0F6cE1QVk9RYjZGZ3RqQWlw
RnFERXB2MGlXVU5iWWJDTzhDYlNmVVZodnBENEVEQ2FmaTZxZTVlN3ZVNHlR
Vkt6M0hiSXZvQVdOWFRPc2JaQzJTTVpTVkFvMjV0MD0iLCJrZXkiOiJFZG1E
QVpFclpoZWF6YmJKT3BxOCt0bnRza1dCdFU5T1hudllyaEtJSEdKUFZxTHQ4
NHV2Mjd5RS94a1d5dnVxNFRjUlN4Q0JKTDJnOEg2QitjT1V6RjAxV215bzZ6
MVpEenFqcm1neksyS2RkZzZabm50bWRDTnZFRVVGeW5BTGs1VkN2LyszUis2
Y2JSK2pCTkxBc2NhbGY1WE9vYUxWQ1JzTzQ0c251TGNid1JFYTdSVm84b09D
bkNsUTZYMzRoUXBYRG52R0hXWTJPMVpscXhGZ1FwWE5MUHEyUDlxN2VQbTZp
a1RJV3lwL0Nhc3Y0cjhEQS9LWE4zaXUwcGlEaS80eFR5NFpiNXk4RXJvQUt3
emJFTHVYL1NDeU9XWWoyamN4WjluQkcxd05tNmsvbnd5N25wSC9uODduTklj
WFBUOW9BL0RQcWU4c0NkYnBSV0x3L290cElqYVc3Nk5GRzFSbmNiOHN5STJl
Zi9PZFZBZkFhMU1CMFNrcEtVR0w0RGlPQkl4T3FrK2ZvRUpjWEdTallGUTJN
M1Z0VUdNMjhFaHZOTnlVT01XY1ZYV2h2cDZGYVVVRXp6d0pYcy9kQ1dvSXBN
L2E1ZnpHYjR3MjBXWHZxbGpYc1hac2dRYUx0bDk2aGd4TVB2d0gwREdncjhl
bEh4WldqRFR4VEozbXNxR1drYXkzRUVWUmxwY0dtQjJ2aGViUjJxVUNWY1JC
Um9PZmszZmVQMFN4V21KMVVMaE1XVHdBUDAxRG85QTdQV2pMVWdoeEpmc0Rh
ekNrUDBJVk0wbk5LRi82T3JBc3hibkpiUXdzVk85OGlqcEJ5OFlwT0d3RWFo
RGZ1UXJGQW56VE1aWkhIRWRwQUFMcXIwNFJRMGxEZUtlaHVYUHB4MlhzVm1O
UWs0Yz0iLCJpdiI6InYwZmtWWlVlZ29BL3RUN084MGhoWUE9PSJ9
-----END OPENPROJECT-EE TOKEN-----'
To generate the enterprise-token, follow the directions in the momo/openproject-token-js repository.
Forgejo
Self-hosted lightweight software forge. https://forgejo.org
Prerequisites
To allow SSH access to forgejo’s built-in SSH server, a firewall rule needs to
be added to the host using the correct interface name. In the below usage
example, the interface name is enp3s0.
Usage
momo-cloud.forgejo = {
enable = true;
sshListenInterface = "enp3s0";
sshServerHostKeysFile = config.age.secrets."forgejo-ssh-host-key".path;
repositorySigningPublicKeyFile = config.age.secrets."forgejo-repository-signing-key.pub".path;
repositorySigningPrivateKeyFile = config.age.secrets."forgejo-repository-signing-key".path;
oidcSecretFile = config.age.secrets."forgejo-oidc-secret".path;
};
There are two kanidm groups for using forgejo:
forgejo_admins: grants admin rights (not implemented)forgejo_users: allows users to access the application
Secrets
oidcSecretFile
Generate the OIDC secret, used by services.kanidm.provision:
openssl rand -hex 24
sshServerHostKeysFile
Generate the SSH server host key with an empty passphrase, used by services.forgejo.settings.server.SSH_SERVER_HOST_KEYS:
ssh-keygen -t ed25519
repositorySigningPublicKeyFile and repositorySigningPrivateKeyFile
Generate the repository signing key with an empty passphrase, used by services.forgejo.settings."repository.signing".SIGNING_KEY
Set this to the public key path. Note that the value for the SIGNING_KEY setting needs to be an absolute path to the public key, where the private key needs to be available in the same directory without the .pub suffix.
ssh-keygen -t ed25519
Troubleshooting
After initial deployment, a restart of nginx.service might be required because the nginx UNIX user needs to be part of the anubis group.
Forgejo
Self-hosted CI Actions runners for forgejo. https://forgejo.org
momo-cloud includes defaults for gitea-runners. They are automatically activated once you enable an instance.
This includes:
- Enabling the docker service
- Adding a user gitea-runner for the runners
- Adding bridges to the trusted firewall interfaces
Usage
services.gitea-actions-runner.instances = {
atlas = {
enable = true;
url = "https://git.${config.networking.hostName}";
name = config.networking.hostName;
tokenFile = config.age.secrets.forgejo-actions-runner-token.path;
labels = [
"self-hosted:host"
"fast-self-hosted:host"
];
};
};
Secrets
tokenFile
Found in forgejo as “Registration Token”, prepend with TOKEN=:
TOKEN=<registration token>