Compare commits

...

28 Commits

Author SHA1 Message Date
12d014eda9 Allow force destroying an instance. 2026-07-27 16:21:01 -07:00
1c57b865b7 Fixing dashboard error when instance in progress. 2026-07-27 16:20:46 -07:00
85ad1c9311 Refactor authelia for logs to use file backed users. 2026-07-27 16:20:28 -07:00
38d718533c Fixing terraform race condition. 2026-07-27 16:19:19 -07:00
cf1edb5d58 Upgrading terraform providers. 2026-07-27 16:18:34 -07:00
609a315c25 Adding "instance-up" deployment status. 2026-07-27 16:18:05 -07:00
bc301d6b29 Making the dashboard app display less confusing. 2026-07-25 11:02:09 -07:00
9f89356c85 Don't try to make a backup on inital deployment. 2026-07-25 11:00:20 -07:00
2721249a89 User selects backblaze bucket instead of providing bucket url 2026-07-25 10:02:25 -07:00
a64247de4e Reworking backblaze bucket delete to handle all key types 2026-07-24 20:54:14 -07:00
420a7756af Working delete instance for all cases. 2026-07-24 16:27:42 -07:00
1a0252de1a Adding better support for undeployed instances. 2026-07-24 12:29:22 -07:00
d74e913023 Fixing nextcloud validation. 2026-07-24 12:28:35 -07:00
7ffbe713e9 Fixing restic call for non-docker envs 2026-07-24 12:28:15 -07:00
1a60f94b13 Improving theme, cleaning UI, adding validations. 2026-07-20 21:41:47 -07:00
8173b798a8 Re-sync db-init.sql 2026-07-20 21:41:01 -07:00
b492ff208f Fixing nassella-in-production bugs. 2026-07-15 15:18:34 -07:00
321578a74a Fixing backblaze connection test in prod setups. 2026-07-15 12:19:18 -07:00
7236c8854d Fixing create-instance after app db refactor 2026-07-15 12:18:38 -07:00
7ee27178f9 Fixing missing Nassella make rules 2026-07-15 12:18:23 -07:00
f9bf59aa56 Fixing ghost docker config 2026-07-11 16:42:10 -07:00
1ff8b5348d Bumping up flatcar version. 2026-07-11 14:37:55 -07:00
ce4882816b Properly escape apps.config 2026-07-11 14:37:43 -07:00
ce948a86ce Add retention policy for restic. 2026-07-11 13:44:26 -07:00
7bca829134 Actually test backblaze connection in wizard. 2026-07-11 13:07:39 -07:00
1019377cd8 Re-enable backups before deploy and cleanup old TODOs 2026-07-10 18:31:14 -07:00
4115922257 Moving /app tree in ignition to gzipped file 2026-07-10 18:04:13 -07:00
7ff7be6819 Adding back accidentally removed terraform lock 2026-07-10 18:03:49 -07:00
25 changed files with 1289 additions and 578 deletions

96
.terraform.lock.hcl generated Normal file
View File

@@ -0,0 +1,96 @@
# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.
provider "registry.terraform.io/cloudflare/cloudflare" {
version = "5.8.2"
constraints = "5.8.2"
hashes = [
"h1:q9p622Nl/BMW9QUlnGIgFaYF7O94u7bfif48OQ0H9HE=",
"zh:92d133a074a8a95143861960508501711a95e0b267c60dbc722eec0a68053e08",
"zh:95b1f349c59d226d9005c49b5ac61e856492f75d8be3ca30954a91512f2ebecf",
"zh:a439ebd2e21dd6a4cd782563de372dbbac9853a713ba4e90fb3d317764374c2f",
"zh:b0a2313cdb113e8ecc058052bcbadaa312a6d093820e69ed97ed96725ad5fd8f",
"zh:c14e2790827a78f0e5274ec47f56e4fff41b76db01caf915fb73d5082c75c645",
"zh:ef135fce7d3925988fa2675287788da233a1630c23652ae160a1d6c2a9e8ebce",
"zh:f01d39efcb3aa7fa8634394caad10ca68abd694d75d2c8b3875516859d8df9b4",
"zh:f03391264e01749c17ead5a8ba33a96a568490a8212424c5ae6e113ad50bfc9f",
"zh:f809ab383cca0a5f83072981c64208cbd7fa67e986a86ee02dd2c82333221e32",
]
}
provider "registry.terraform.io/digitalocean/digitalocean" {
version = "2.62.0"
constraints = "2.62.0"
hashes = [
"h1:MPWqSuVPLh6SNduEpKYZHOXDkW4CCtfEOcZMSjKrGnA=",
"zh:1004bd408d8982471a7b0971fb2c47ccb3e9b8b3261422a6c8942151e6624713",
"zh:173059dd17ab6cb471dd45ee0e2a01db9c2e7d31ac30e33eb3b6269cfca68bbb",
"zh:209e8092f3cfcb28406df19386064e31ab89152dc70555db2e252dc1ea9e5703",
"zh:2c45405951b64ca3606d4360463eee4745af361520f9a8e28b5e6e784010dc4b",
"zh:3b07459b33e56d2cfe2429dc499eb33e83969f75847dcab9a65b8de8cdefebcc",
"zh:486277719db85e0062a7a0014bfd0c9a2eda0c2955d9c77020a35f004bc4025f",
"zh:7487b77f0e0d33ca460f27cf8eeae08cce50bb4d64a693601cd76ca865d3eb78",
"zh:7ac4818386b45253b2e3f8b2794fb0a0837878a37bf03ace4e7a6c7f69345abc",
"zh:9a8ad3875867cb707ab50bf0c10fe23abdf95740f0fd48ee8575586aaf9e9817",
"zh:9b39578e77688b367f5f99ae4227cf76edb598263937983652b0e7052fad6b65",
"zh:a72723121d4548f17cfba7cc14d2cf9cc3ce066c1c81121ceac7dee42eeb84ee",
"zh:af2cf2b807af245c4259cd4e9980fe6633e9a3703e114b4997f8ee8a56188674",
"zh:b547d28e1e2de2c03ffde90eeb2d301331520b890c219e84aa876263ebf6e4fd",
"zh:baa2e3c085de1154fdef4922bc6c27aa62f0dba60535ec80e10017909ea93e2d",
"zh:c8425a8cfcba3ee408d9b75586f02f5c9af9c9d01817227731078662720e932f",
"zh:dfdfb75ff50d79de30164873c3977dce376ffc09ce5610aeb581f885b3f8b942",
]
}
provider "registry.terraform.io/hashicorp/null" {
version = "3.2.4"
constraints = "3.2.4"
hashes = [
"h1:wTNrZnwQdOOT/TW9pa+7GgJeFK2OvTvDmx78VmUmZXM=",
"zh:59f6b52ab4ff35739647f9509ee6d93d7c032985d9f8c6237d1f8a59471bbbe2",
"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
"zh:795c897119ff082133150121d39ff26cb5f89a730a2c8c26f3a9c1abf81a9c43",
"zh:7b9c7b16f118fbc2b05a983817b8ce2f86df125857966ad356353baf4bff5c0a",
"zh:85e33ab43e0e1726e5f97a874b8e24820b6565ff8076523cc2922ba671492991",
"zh:9d32ac3619cfc93eb3c4f423492a8e0f79db05fec58e449dee9b2d5873d5f69f",
"zh:9e15c3c9dd8e0d1e3731841d44c34571b6c97f5b95e8296a45318b94e5287a6e",
"zh:b4c2ab35d1b7696c30b64bf2c0f3a62329107bd1a9121ce70683dec58af19615",
"zh:c43723e8cc65bcdf5e0c92581dcbbdcbdcf18b8d2037406a5f2033b1e22de442",
"zh:ceb5495d9c31bfb299d246ab333f08c7fb0d67a4f82681fbf47f2a21c3e11ab5",
"zh:e171026b3659305c558d9804062762d168f50ba02b88b231d20ec99578a6233f",
"zh:ed0fe2acdb61330b01841fa790be00ec6beaac91d41f311fb8254f74eb6a711f",
]
}
provider "registry.terraform.io/hashicorp/time" {
version = "0.13.1"
constraints = "0.13.1"
hashes = [
"h1:P9h9GNlrWPECzIvIFjHOhF+HVzpxk0eCcdy1G0fWSHw=",
"zh:02cb9aab1002f0f2a94a4f85acec8893297dc75915f7404c165983f720a54b74",
"zh:04429b2b31a492d19e5ecf999b116d396dac0b24bba0d0fb19ecaefe193fdb8f",
"zh:26f8e51bb7c275c404ba6028c1b530312066009194db721a8427a7bc5cdbc83a",
"zh:772ff8dbdbef968651ab3ae76d04afd355c32f8a868d03244db3f8496e462690",
"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
"zh:898db5d2b6bd6ca5457dccb52eedbc7c5b1a71e4a4658381bcbb38cedbbda328",
"zh:8de913bf09a3fa7bedc29fec18c47c571d0c7a3d0644322c46f3aa648cf30cd8",
"zh:9402102c86a87bdfe7e501ffbb9c685c32bbcefcfcf897fd7d53df414c36877b",
"zh:b18b9bb1726bb8cfbefc0a29cf3657c82578001f514bcf4c079839b6776c47f0",
"zh:b9d31fdc4faecb909d7c5ce41d2479dd0536862a963df434be4b16e8e4edc94d",
"zh:c951e9f39cca3446c060bd63933ebb89cedde9523904813973fbc3d11863ba75",
"zh:e5b773c0d07e962291be0e9b413c7a22c044b8c7b58c76e8aa91d1659990dfb5",
]
}
provider "registry.terraform.io/poseidon/ct" {
version = "0.13.0"
constraints = "0.13.0"
hashes = [
"h1:sTtlmMjbYiIuaad/23wVrOCSlihKaurcf2668M3ZWio=",
"zh:24d86adcba92ad0f13870d5e0d217c395aa90ff1e9234fe0c9b7c6eb65abb3a8",
"zh:317eeadf92d220fe546be624a9002190edeb623ac76ae7f6a93abd9fe1be65fd",
"zh:361dbff802ccbd94b87c9d77c0d9db9bdf4d5d408f8cf05e4dae203e60b310ca",
"zh:3b25cb8a0327886aa30c273561ecea3315cc4d729677cd6528ed1339486475da",
"zh:63455a68fee4ba0c9b131eb7e267eb17707184c55a5feb9e2bad2d9de5889d6a",
]
}

View File

@@ -19,6 +19,12 @@ TERRAFORM_ENV := production
config_dir := ./config/ config_dir := ./config/
apps_config := $(config_dir)apps.config apps_config := $(config_dir)apps.config
BUTANE := butane -d ${PWD}
ifeq ($(shell command -v docker >/dev/null 2>&1 && echo yes),yes)
BUTANE := docker run --rm --volume ${PWD}:/pwd --workdir /pwd -i quay.io/coreos/butane:latest -d /pwd
endif
IMAGE_DEPS := IMAGE_DEPS :=
COMPOSE_ENVS := $(shell if [ -d app/ ]; then find app/ -type f -name '.compose-env'; fi) COMPOSE_ENVS := $(shell if [ -d app/ ]; then find app/ -type f -name '.compose-env'; fi)
@@ -66,6 +72,12 @@ restic-env: $(apps_config) make-restic-generated.sh
restic-password: $(apps_config) make-restic-password.sh restic-password: $(apps_config) make-restic-password.sh
./make-restic-password.sh $(apps_config) > restic-password ./make-restic-password.sh $(apps_config) > restic-password
app.tar.gz: app/.dirstamp \
app/.env \
app/lb/lb.env \
app/lb/Caddyfile
tar cfz app.tar.gz app/
ignition.json: cl.yaml app/.dirstamp \ ignition.json: cl.yaml app/.dirstamp \
$(IMAGE_DEPS) \ $(IMAGE_DEPS) \
app/lb/lb.env \ app/lb/lb.env \
@@ -74,9 +86,11 @@ restic-env \
restic-password \ restic-password \
restic-restore.sh \ restic-restore.sh \
restic-snapshot.sh \ restic-snapshot.sh \
restic-scheduled-snapshot.sh \
app/.env \ app/.env \
$(config_dir)ssh-keys $(config_dir)ssh-keys \
cat cl.yaml | docker run --rm --volume $(config_dir)/ssh-keys:/pwd/ssh-keys --volume ${PWD}:/pwd --workdir /pwd -i quay.io/coreos/butane:latest -d /pwd > ignition.json app.tar.gz
cat cl.yaml | $(BUTANE) > ignition.json
generated.tfvars: $(apps_config) make-generated.sh generated.tfvars: $(apps_config) make-generated.sh
./make-generated.sh $(apps_config) > generated.tfvars ./make-generated.sh $(apps_config) > generated.tfvars
@@ -97,11 +111,11 @@ preapply: app/.dirstamp
apply: announce-start restic-init ignition.json $(config_dir)$(TERRAFORM_ENV).tfvars generated.tfvars apply: announce-start restic-init ignition.json $(config_dir)$(TERRAFORM_ENV).tfvars generated.tfvars
echo "NASSELLA_CONFIG: end" echo "NASSELLA_CONFIG: end"
terraform init terraform init --upgrade
bash -c "terraform apply -auto-approve -input=false -var-file=<(cat $(config_dir)$(TERRAFORM_ENV).tfvars generated.tfvars)" bash -c "terraform apply -auto-approve -input=false -var-file=<(cat $(config_dir)$(TERRAFORM_ENV).tfvars generated.tfvars)"
destroy: ignition.json $(config_dir)$(TERRAFORM_ENV).tfvars generated.tfvars destroy: ignition.json $(config_dir)$(TERRAFORM_ENV).tfvars generated.tfvars
terraform init terraform init --upgrade
bash -c "terraform destroy -auto-approve -input=false -var-file=<(cat $(config_dir)$(TERRAFORM_ENV).tfvars generated.tfvars)" bash -c "terraform destroy -auto-approve -input=false -var-file=<(cat $(config_dir)$(TERRAFORM_ENV).tfvars generated.tfvars)"
.PHONY: restic-init .PHONY: restic-init
@@ -120,6 +134,7 @@ archive:
echo ")" >> all-apps/root-napp.scm echo ")" >> all-apps/root-napp.scm
tar -cf nassella-latest.tar all-apps cl.yaml init-restic.sh main.tf make-caddyfile-env.sh make-top-level-docker-compose.sh Makefile \ tar -cf nassella-latest.tar all-apps cl.yaml init-restic.sh main.tf make-caddyfile-env.sh make-top-level-docker-compose.sh Makefile \
make-generated.sh make-restic-generated.sh make-restic-password.sh restic-snapshots.sh copy-apps.sh restic-restore.sh restic-snapshot.sh \ make-generated.sh make-restic-generated.sh make-restic-password.sh restic-snapshots.sh copy-apps.sh restic-restore.sh restic-snapshot.sh \
restic-scheduled-snapshot.sh \
.terraform.lock.hcl .terraform.lock.hcl
cp nassella-latest.tar src/ cp nassella-latest.tar src/

View File

@@ -14,8 +14,17 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with Nassella. If not, see <https://www.gnu.org/licenses/>. # along with Nassella. If not, see <https://www.gnu.org/licenses/>.
{$authelia_SUBDOMAIN}.{$ROOT_DOMAIN} { #{$authelia_SUBDOMAIN}.{$ROOT_DOMAIN} {
import maintenance_intercept # route {
# @authelia path /authelia /authelia/*
# handle @authelia {
# reverse_proxy authelia:9091
# }
# forward_auth authelia:9091 {
# uri /api/authz/forward-auth
# copy_headers Remote-User Remote-Groups Remote-Email Remote-Name
# }
# reverse_proxy authelia:9091
# }
#}
reverse_proxy authelia:9091
}

View File

@@ -18,7 +18,8 @@ IMAGE_DEPS := $(IMAGE_DEPS) \
app/authelia/postgres_db \ app/authelia/postgres_db \
app/authelia/postgres_user \ app/authelia/postgres_user \
app/authelia/postgres_password \ app/authelia/postgres_password \
app/authelia/authelia-config/configuration.yml app/authelia/authelia-config/configuration.yml \
app/authelia/authelia-config/users.yml
app/authelia/postgres_db: $(apps_config) app/authelia/postgres_db: $(apps_config)
bash -c 'source ./$(apps_config); printf "%s\n" "$$AUTHELIA_POSTGRES_DB" > $@' bash -c 'source ./$(apps_config); printf "%s\n" "$$AUTHELIA_POSTGRES_DB" > $@'
@@ -29,3 +30,6 @@ app/authelia/postgres_password: $(apps_config)
app/authelia/authelia-config/configuration.yml: $(apps_config) app/authelia/authelia-config/configuration.yml.tmpl \ app/authelia/authelia-config/configuration.yml: $(apps_config) app/authelia/authelia-config/configuration.yml.tmpl \
app/authelia/make-authelia-config.sh app/authelia/make-authelia-config.sh
./app/authelia/make-authelia-config.sh $(apps_config) ./app/authelia/make-authelia-config.sh $(apps_config)
app/authelia/authelia-config/users.yml: $(apps_config) app/authelia/authelia-config/users.yml.tmpl \
app/authelia/make-authelia-users-config.sh
./app/authelia/make-authelia-users-config.sh $(apps_config)

View File

@@ -35,58 +35,45 @@ identity_validation:
reset_password: reset_password:
jwt_secret: '$AUTHELIA_JWT_SECRET' jwt_secret: '$AUTHELIA_JWT_SECRET'
# lldap service account user should instead
# use an account with lldap_password_manager group
# since that can't be used to change an admin password
authentication_backend: authentication_backend:
ldap: disable_reset_password: true
address: 'ldap://lldap:3890' file:
implementation: 'lldap' path: '/config/users.yml'
timeout: '5s' watch: false
pooling: search:
enable: false email: false
count: 5 case_insensitive: false
retries: 2 extra_attributes:
timeout: '10 seconds' extra_example:
base_dn: 'DC=nassella,DC=org' multi_valued: false
# additional_users_dn: 'OU=users' value_type: 'string'
# additional_groups_dn: 'OU=groups' password:
# group_search_mode: 'filter' algorithm: 'argon2'
# permit_referrals: false argon2:
permit_unauthenticated_bind: false variant: 'argon2id'
permit_feature_detection_failure: false iterations: 3
user: 'uid=admin,ou=people,dc=nassella,dc=org' memory: 65536
password: '$LLDAP_ADMIN_PASSWORD' parallelism: 4
# attributes: key_length: 32
# distinguished_name: 'distinguishedName' salt_length: 16
# username: 'uid' scrypt:
# display_name: 'displayName' variant: 'scrypt'
# family_name: 'sn' iterations: 16
# given_name: 'givenName' block_size: 8
# middle_name: 'middleName' parallelism: 1
# nickname: '' key_length: 32
# gender: '' salt_length: 16
# birthdate: '' pbkdf2:
# website: 'wWWHomePage' variant: 'sha512'
# profile: '' iterations: 310000
# picture: '' salt_length: 16
# zoneinfo: '' sha2crypt:
# locale: '' variant: 'sha512'
# phone_number: 'telephoneNumber' iterations: 50000
# phone_extension: '' salt_length: 16
# street_address: 'streetAddress' bcrypt:
# locality: 'l' variant: 'standard'
# region: 'st' cost: 12
# postal_code: 'postalCode'
# country: 'c'
# mail: 'mail'
# member_of: 'memberOf'
# group_name: 'cn'
# extra:
# extra_example:
# name: ''
# multi_valued: false
# value_type: 'string'
access_control: access_control:
default_policy: 'deny' default_policy: 'deny'
@@ -102,11 +89,12 @@ session:
secret: '$AUTHELIA_SESSION_SECRET' secret: '$AUTHELIA_SESSION_SECRET'
cookies: cookies:
- name: 'authelia_session' - name: 'dozzle_session'
domain: '$ROOT_DOMAIN' # Should match whatever your root protected domain is domain: '$DOZZLE_FULL_DOMAIN' # Should match whatever your root protected domain is
authelia_url: 'https://$AUTHELIA_FULL_DOMAIN' authelia_url: 'https://$DOZZLE_FULL_DOMAIN/authelia'
expiration: '1 hour' # 1 hour expiration: '1 hour' # 1 hour
inactivity: '5 minutes' # 5 minutes inactivity: '5 minutes' # 5 minutes
default_redirection_url: 'https://$DOZZLE_FULL_DOMAIN/'
regulation: regulation:
max_retries: 3 max_retries: 3

View File

@@ -0,0 +1,7 @@
# yaml-language-server: $schema=https://www.authelia.com/schemas/latest/json-schema/user-database.json
users:
admin:
disabled: false
displayname: 'Admin'
password: '$HOST_ADMIN_PASSWORD'
email: '$HOST_ADMIN_USER'

View File

@@ -50,13 +50,11 @@ services:
image: 'authelia/authelia' image: 'authelia/authelia'
volumes: volumes:
- ./authelia/authelia-config/configuration.yml:/config/configuration.yml:ro - ./authelia/authelia-config/configuration.yml:/config/configuration.yml:ro
- ./authelia/authelia-config/users.yml:/config/users.yml:ro
networks: networks:
- lb - lb
- authelia_internal_db - authelia_internal_db
- lldap_internal
depends_on: depends_on:
lldap:
condition: service_healthy
authelia_db: authelia_db:
condition: service_healthy condition: service_healthy
restart: 'unless-stopped' restart: 'unless-stopped'
@@ -66,9 +64,6 @@ services:
networks: networks:
lb: lb:
lldap_internal:
driver: bridge
internal: true
authelia_internal_db: authelia_internal_db:
driver: bridge driver: bridge
internal: true internal: true

View File

@@ -0,0 +1,24 @@
#!/bin/bash
# Copyright 2025-2026 Thomas Hintz
# This file is part of Nassella.
# Nassella is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
# Nassella is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
# You should have received a copy of the GNU Affero General Public License
# along with Nassella. If not, see <https://www.gnu.org/licenses/>.
set -e
set -a # export everything in the config for later use by envsubst
. $1 # source the apps.config file with then env vars
envsubst < app/authelia/authelia-config/users.yml.tmpl > app/authelia/authelia-config/users.yml

View File

@@ -15,9 +15,15 @@
# along with Nassella. If not, see <https://www.gnu.org/licenses/>. # along with Nassella. If not, see <https://www.gnu.org/licenses/>.
{$dozzle_SUBDOMAIN}.{$ROOT_DOMAIN} { {$dozzle_SUBDOMAIN}.{$ROOT_DOMAIN} {
route {
@authelia path /authelia /authelia/*
handle @authelia {
reverse_proxy authelia:9091
}
forward_auth authelia:9091 { forward_auth authelia:9091 {
uri /api/authz/forward-auth uri /api/authz/forward-auth
copy_headers Remote-User Remote-Groups Remote-Email Remote-Name copy_headers Remote-User Remote-Groups Remote-Email Remote-Name
} }
reverse_proxy http://dozzle:8080 reverse_proxy http://dozzle:8080
} }
}

View File

@@ -35,7 +35,7 @@
mail__transport=SMTP mail__transport=SMTP
# mail__options__host= # mail__options__host=
# mail__options__port= # mail__options__port=
mail__options__secure=true # mail__options__secure=true
# mail__options__auth__user= # mail__options__auth__user=
# mail__options__auth__pass= # mail__options__auth__pass=
# mail__from="" # mail__from=""

View File

@@ -2,7 +2,8 @@ services:
ghost: ghost:
image: ghost:${GHOST_VERSION:-6-alpine} image: ghost:${GHOST_VERSION:-6-alpine}
restart: always restart: always
# This is required to import current config when migrating env_file:
- /app/.env
environment: environment:
NODE_ENV: production NODE_ENV: production
url: https://${GHOST_DOMAIN:?GHOST_DOMAIN environment variable is required} url: https://${GHOST_DOMAIN:?GHOST_DOMAIN environment variable is required}

View File

@@ -24,6 +24,8 @@ app/nassella/lldap_postgres_password \
app/nassella/authelia_postgres_db \ app/nassella/authelia_postgres_db \
app/nassella/authelia_postgres_user \ app/nassella/authelia_postgres_user \
app/nassella/authelia_postgres_password \ app/nassella/authelia_postgres_password \
app/nassella/lldap_admin_password \
app/nassella/stripe_api_key \
app/nassella/nassella.env \ app/nassella/nassella.env \
app/nassella/authelia-config/configuration.yml \ app/nassella/authelia-config/configuration.yml \
app/nassella/lldap-config/lldap_config.toml app/nassella/lldap-config/lldap_config.toml

View File

@@ -84,7 +84,7 @@ access_control:
policy: 'two_factor' policy: 'two_factor'
session: session:
secret: 'insecure_session_secret' secret: 'insecure_session_secret' # TODO fix
cookies: cookies:
- name: 'authelia_session' - name: 'authelia_session'

39
cl.yaml
View File

@@ -20,7 +20,7 @@ passwd:
users: users:
- name: core - name: core
ssh_authorized_keys_local: ssh_authorized_keys_local:
- /ssh-keys - config/ssh-keys
- name: nextcloud - name: nextcloud
uid: 1001 uid: 1001
systemd: systemd:
@@ -65,7 +65,7 @@ systemd:
[Service] [Service]
Type=oneshot Type=oneshot
EnvironmentFile=/restic-env EnvironmentFile=/restic-env
ExecStart=/usr/bin/bash -c "docker run --rm --volume /nassella:/nassella --volume /restic-password:/restic-password -e AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} -e AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} -i restic/restic:0.18.0 backup --verbose --repo s3:${BACKBLAZE_BUCKET_URL} --password-file /restic-password --tag daily_automatic /nassella" ExecStart=/restic-scheduled-snapshot.sh
ExecStopPost=systemctl start app.service ExecStopPost=systemctl start app.service
- name: restic-backup.timer - name: restic-backup.timer
@@ -80,6 +80,18 @@ systemd:
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target
- name: unpack-app.service
enabled: true
contents: |
[Unit]
Description=Unpack /app
Before=app.service
[Service]
Type=oneshot
ExecStart=/unpack-app
[Install]
WantedBy=multi-user.target
### docker-compose sysext ### docker-compose sysext
### https://flatcar.github.io/sysext-bakery/docker_compose/ ### https://flatcar.github.io/sysext-bakery/docker_compose/
- name: systemd-sysupdate.timer - name: systemd-sysupdate.timer
@@ -117,10 +129,14 @@ storage:
- device: /dev/disk/by-partlabel/appstorage - device: /dev/disk/by-partlabel/appstorage
format: ext4 format: ext4
wipe_filesystem: false wipe_filesystem: false
trees: # trees:
- path: /app # - path: /app
local: app # local: app
files: files:
- path: /app.tar.gz
mode: 0600
contents:
local: app.tar.gz
- path: /restic-password - path: /restic-password
contents: contents:
local: restic-password local: restic-password
@@ -136,6 +152,10 @@ storage:
mode: 0755 mode: 0755
contents: contents:
local: restic-snapshot.sh local: restic-snapshot.sh
- path: /restic-scheduled-snapshot.sh
mode: 0755
contents:
local: restic-scheduled-snapshot.sh
- path: /etc/ssh/sshd_config.d/custom.conf - path: /etc/ssh/sshd_config.d/custom.conf
overwrite: true overwrite: true
mode: 0600 mode: 0600
@@ -143,6 +163,15 @@ storage:
inline: | inline: |
PermitRootLogin no PermitRootLogin no
AllowUsers core AllowUsers core
- path: /unpack-app
mode: 0755
contents:
inline: |
#!/bin/sh
set -e
mkdir /app
tar -xzf /app.tar.gz -C /
rm /app.tar.gz
### docker-compose sysext ### docker-compose sysext
### https://flatcar.github.io/sysext-bakery/docker_compose/ ### https://flatcar.github.io/sysext-bakery/docker_compose/
- path: /opt/extensions/docker-compose/docker-compose-5.1.4-x86-64.raw - path: /opt/extensions/docker-compose/docker-compose-5.1.4-x86-64.raw

View File

@@ -33,7 +33,7 @@ if [ $? -ne 0 ]; then
if [ $status -eq 10 ]; then if [ $status -eq 10 ]; then
# restic repo is not initialized so initialize it # restic repo is not initialized so initialize it
AWS_ACCESS_KEY_ID="$BACKBLAZE_KEY_ID" -e AWS_SECRET_ACCESS_KEY="$BACKBLAZE_APPLICATION_KEY" restic init --repo s3:$BACKBLAZE_BUCKET_URL --password-file restic-password AWS_ACCESS_KEY_ID="$BACKBLAZE_KEY_ID" AWS_SECRET_ACCESS_KEY="$BACKBLAZE_APPLICATION_KEY" restic init --repo s3:$BACKBLAZE_BUCKET_URL --password-file restic-password
init_status=$? init_status=$?
elif [ $status -ne 0 ]; then elif [ $status -ne 0 ]; then
# something unexpected happened, exit # something unexpected happened, exit

28
main.tf
View File

@@ -2,23 +2,19 @@ terraform {
required_providers { required_providers {
digitalocean = { digitalocean = {
source = "digitalocean/digitalocean" source = "digitalocean/digitalocean"
version = "2.62.0" version = "2.96.0"
} }
ct = { ct = {
source = "poseidon/ct" source = "poseidon/ct"
version = "0.13.0" version = "0.14.0"
} }
null = { null = {
source = "hashicorp/null" source = "hashicorp/null"
version = "3.2.4" version = "3.3.0"
}
time = {
source = "hashicorp/time"
version = "0.13.1"
} }
cloudflare = { cloudflare = {
source = "cloudflare/cloudflare" source = "cloudflare/cloudflare"
version = "5.8.2" version = "5.22.0"
} }
} }
} }
@@ -86,6 +82,10 @@ variable "digitalocean_volume_size" {
provider "digitalocean" { provider "digitalocean" {
token = var.do_token token = var.do_token
http_retry_max = 10
http_retry_wait_min = 2
http_retry_wait_max = 60
} }
provider "cloudflare" { provider "cloudflare" {
@@ -142,21 +142,11 @@ resource "digitalocean_droplet" "machine" {
size = var.server_type size = var.server_type
ssh_keys = [digitalocean_ssh_key.first.fingerprint] ssh_keys = [digitalocean_ssh_key.first.fingerprint]
user_data = file("ignition.json") user_data = file("ignition.json")
lifecycle {
create_before_destroy = true
} }
}
resource "time_sleep" "wait_10_seconds" {
create_duration = "10s"
}
resource "digitalocean_reserved_ip_assignment" "machine" { resource "digitalocean_reserved_ip_assignment" "machine" {
depends_on = [digitalocean_volume_attachment.machine]
ip_address = digitalocean_reserved_ip.machine.ip_address ip_address = digitalocean_reserved_ip.machine.ip_address
droplet_id = digitalocean_droplet.machine.id droplet_id = digitalocean_droplet.machine.id
# sometimes digital ocean throws an error for this resource
# saying the droplet has a pending event so we delay a few seconds
depends_on = [time_sleep.wait_10_seconds]
} }
resource "digitalocean_volume_attachment" "machine" { resource "digitalocean_volume_attachment" "machine" {

View File

@@ -31,8 +31,8 @@ set -e
. $1 # source the apps.config file with then env vars . $1 # source the apps.config file with then env vars
host_admin_password_encoded=`echo "$HOST_ADMIN_PASSWORD" | docker run --rm -i caddy:2 caddy hash-password` # host_admin_password_encoded=`echo "$HOST_ADMIN_PASSWORD" | docker run --rm -i caddy:2 caddy hash-password`
host_admin_password_encoded="password" # unused
read -r -a APP_CONFIGS <<< "$APP_CONFIGS" read -r -a APP_CONFIGS <<< "$APP_CONFIGS"
APP_CONFIGS+=('lb,root') APP_CONFIGS+=('lb,root')

22
restic-scheduled-snapshot.sh Executable file
View File

@@ -0,0 +1,22 @@
#!/bin/bash
# Copyright 2025-2026 Thomas Hintz
# This file is part of Nassella.
# Nassella is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
# Nassella is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
# You should have received a copy of the GNU Affero General Public License
# along with Nassella. If not, see <https://www.gnu.org/licenses/>.
docker run --rm --volume /nassella:/nassella --volume /restic-password:/restic-password -e AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} -e AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} -i restic/restic:0.18.0 backup --verbose --repo s3:${BACKBLAZE_BUCKET_URL} --password-file /restic-password --tag daily_automatic /nassella
# only keep one snapshot per day for the last week and one snapshot per month for the last 3 months
docker run --rm --volume /nassella:/nassella --volume /restic-password:/restic-password -e AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} -e AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} -i restic/restic:0.18.0 forget --tag daily_automatic --keep-daily 7 --keep-monthly 3 --verbose --repo s3:${BACKBLAZE_BUCKET_URL} --password-file /restic-password /nassella

View File

@@ -30,7 +30,7 @@ RUN chicken-install srfi-1 srfi-13 srfi-18 srfi-19 srfi-158 srfi-194 \
sxml-transforms schematra \ sxml-transforms schematra \
uri-common http-client medea intarweb \ uri-common http-client medea intarweb \
sql-null openssl postgresql crypto-tools \ sql-null openssl postgresql crypto-tools \
hmac sha2 string-utils hmac sha2 string-utils base64
WORKDIR /var WORKDIR /var
RUN mkdir nassella RUN mkdir nassella
@@ -43,10 +43,19 @@ COPY run.scm run.scm
RUN csc -O3 -o nassella-run run.scm RUN csc -O3 -o nassella-run run.scm
RUN chmod +x nassella-run RUN chmod +x nassella-run
FROM quay.io/coreos/butane:release AS butane
FROM hashicorp/terraform:1.15 AS terraform
FROM authelia/authelia:latest AS authelia
FROM debian:trixie-slim FROM debian:trixie-slim
RUN apt-get update && apt-get -y --no-install-recommends install \ RUN apt-get update && apt-get -y --no-install-recommends install \
libpq-dev ca-certificates gettext-base openssh-client restic \ libpq-dev ca-certificates gettext-base openssh-client restic make \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
COPY --from=butane /usr/local/bin/butane /usr/local/bin/butane
COPY --from=terraform /bin/terraform /usr/local/bin/terraform
COPY --from=authelia /app/authelia /usr/local/bin/authelia
COPY --from=buildeggs /usr/local/ /usr/local/ COPY --from=buildeggs /usr/local/ /usr/local/
WORKDIR /var WORKDIR /var

View File

@@ -45,7 +45,9 @@ create table user_service_configs(
digitalocean_volume_size integer, digitalocean_volume_size integer,
backblaze_application_key_enc varchar(255), backblaze_application_key_enc varchar(255),
backblaze_key_id_enc varchar(255), backblaze_key_id_enc varchar(255),
backblaze_bucket_url_enc varchar(255) backblaze_bucket_url_enc varchar(255), -- deprecated 072526, replaced by bucket_name and bucket_id
backblaze_bucket_name_enc varchar(255),
backblaze_bucket_id_enc varchar(255)
); );
create unique index user_service_configs_user_id_instance_id_idx on user_service_configs (user_id, instance_id); create unique index user_service_configs_user_id_instance_id_idx on user_service_configs (user_id, instance_id);
@@ -62,6 +64,8 @@ insert into apps(app_name) values ('log-viewer');
insert into apps(app_name) values ('ghost'); insert into apps(app_name) values ('ghost');
insert into apps(app_name) values ('instance-control'); insert into apps(app_name) values ('instance-control');
insert into apps(app_name) values ('wordpress'); insert into apps(app_name) values ('wordpress');
insert into apps(app_name) values ('lldap');
insert into apps(app_name) values ('authelia');
create table user_selected_apps( create table user_selected_apps(
id bigserial primary key, id bigserial primary key,
@@ -89,7 +93,7 @@ create table user_app_configs(
create unique index user_app_configs_user_id_instance_id_idx on user_app_configs (user_id, instance_id); create unique index user_app_configs_user_id_instance_id_idx on user_app_configs (user_id, instance_id);
create type deployment_status as enum ('queued', 'in-progress', 'complete', 'failed'); create type deployment_status as enum ('ignored', 'queued', 'in-progress', 'complete', 'failed');
create table deployments( create table deployments(
id bigserial primary key, id bigserial primary key,

View File

@@ -197,7 +197,6 @@ returning users.user_id;"
(user-encrypt-for-db ssh-key-pub user-key user-iv user-id) (user-encrypt-for-db ssh-key-pub user-key user-iv user-id)
(user-encrypt-for-db restic-password user-key user-iv user-id))))) (user-encrypt-for-db restic-password user-key user-iv user-id)))))
(query conn "insert into user_service_configs(user_id, instance_id) values ($1, $2);" user-id instance-id) (query conn "insert into user_service_configs(user_id, instance_id) values ($1, $2);" user-id instance-id)
(query conn "insert into user_selected_apps(user_id, instance_id) values ($1, $2);" user-id instance-id)
(query conn "insert into user_app_configs(user_id, instance_id) values ($1, $2);" user-id instance-id) (query conn "insert into user_app_configs(user_id, instance_id) values ($1, $2);" user-id instance-id)
(query conn "insert into user_terraform_state(user_id, instance_id) values ($1, $2);" user-id instance-id) (query conn "insert into user_terraform_state(user_id, instance_id) values ($1, $2);" user-id instance-id)
instance-id))) instance-id)))
@@ -249,7 +248,9 @@ returning users.user_id;"
(digitalocean-volume-size . ("digitalocean_volume_size" #f)) (digitalocean-volume-size . ("digitalocean_volume_size" #f))
(backblaze-application-key . ("backblaze_application_key_enc" #t)) (backblaze-application-key . ("backblaze_application_key_enc" #t))
(backblaze-key-id . ("backblaze_key_id_enc" #t)) (backblaze-key-id . ("backblaze_key_id_enc" #t))
(backblaze-bucket-url . ("backblaze_bucket_url_enc" #t)))) (backblaze-bucket-url . ("backblaze_bucket_url_enc" #t))
(backblaze-bucket-name . ("backblaze_bucket_name_enc" #t))
(backblaze-bucket-id . ("backblaze_bucket_id_enc" #t))))
(define *user-service-configs-reverse-column-map* (define *user-service-configs-reverse-column-map*
(map (lambda (config) (map (lambda (config)
@@ -377,7 +378,8 @@ where usa.user_id=$1 and usa.instance_id=$2;"
'((queued . "queued") '((queued . "queued")
(in-progress . "in-progress") (in-progress . "in-progress")
(complete . "complete") (complete . "complete")
(failed . "failed"))) (failed . "failed")
(ignored . "ignored")))
(define (create-deployment conn user-id instance-id) (define (create-deployment conn user-id instance-id)
(value-at (value-at
(query conn (query conn
@@ -415,6 +417,7 @@ where usa.user_id=$1 and usa.instance_id=$2;"
(volume-create . "terraform_volume_create") (volume-create . "terraform_volume_create")
(volume-destroy . "terraform_volume_destroy") (volume-destroy . "terraform_volume_destroy")
(instance-backup . "instance_backup") (instance-backup . "instance_backup")
(instance-up . "instance_up")
(status . "status") (status . "status")
(id . "id") (id . "id")
(instance-id . "instance_id"))) (instance-id . "instance_id")))
@@ -516,13 +519,23 @@ where usa.user_id=$1 and usa.instance_id=$2;"
"select apps.app_name, usa.installed_version, usa.instance_id from user_selected_apps usa "select apps.app_name, usa.installed_version, usa.instance_id from user_selected_apps usa
join apps on apps.id=usa.app_id join apps on apps.id=usa.app_id
where user_id=$1;" where user_id=$1;"
user-id)))) user-id)))
(undeployed-res
(query conn
(string-append
"select i.instance_id from instances as i "
"left join deployments d on d.instance_id = i.instance_id "
"where i.user_id=$1 and d.status is null "
"group by d.id, i.instance_id;")
user-id)))
(append
(map (map
(lambda (instance) (lambda (instance)
(cons `(apps . ,(alist-ref (alist-ref 'instance-id instance) instance-apps)) (cons `(apps . ,(alist-ref (alist-ref 'instance-id instance) instance-apps))
instance)) instance))
(map (map
(lambda (row-num) (lambda (row-num)
(cons '(deployed . #t)
(map (lambda (item) (map (lambda (item)
(let* ((key (car item)) (let* ((key (car item))
(value (cdr item)) (value (cdr item))
@@ -545,8 +558,12 @@ where user_id=$1;"
(user-decrypt-from-db value user-key user-iv user-id) (user-decrypt-from-db value user-key user-iv user-id)
read) read)
value)))))) value))))))
(row-alist res row-num))) (row-alist res row-num))))
(iota (row-count res))))))) (iota (row-count res))))
(map (lambda (id) `((instance-id . ,id) (deployed . #f)))
(if (> (row-count undeployed-res) 0)
(row-values undeployed-res)
'()))))))
(define (update-user-terraform-state conn user-id instance-id state backup) (define (update-user-terraform-state conn user-id instance-id state backup)
(receive (user-key user-iv auth-user-id) (receive (user-key user-iv auth-user-id)
@@ -644,7 +661,10 @@ where user_id=$1;"
(4 . "fixing-app-normalization") (4 . "fixing-app-normalization")
(5 . "adding-wordpress-app") (5 . "adding-wordpress-app")
(6 . "adding-lldap-app") (6 . "adding-lldap-app")
(7 . "adding-authelia-app"))) (7 . "adding-authelia-app")
(8 . "adding-backblaze-bucket-name-and-id")
(9 . "extend-deployment-status-add-ignored")
(10 . "adding-deployments-instance-up")))
(define (run-pending-migrations conn) (define (run-pending-migrations conn)
(let* ((migration-ids (sort (map car *migrations*) <)) (let* ((migration-ids (sort (map car *migrations*) <))

View File

@@ -0,0 +1 @@
alter table deployments add instance_up deployment_status not null default 'queued';

View File

@@ -0,0 +1,2 @@
alter table user_service_configs add backblaze_bucket_name_enc varchar(255);
alter table user_service_configs add backblaze_bucket_id_enc varchar(255);

View File

@@ -0,0 +1 @@
alter type deployment_status add value 'ignored';

File diff suppressed because it is too large Load Diff