diff --git a/all-apps/authelia/4/Caddyfile b/all-apps/authelia/4/Caddyfile index 7965d23..0e1d5a7 100644 --- a/all-apps/authelia/4/Caddyfile +++ b/all-apps/authelia/4/Caddyfile @@ -14,8 +14,17 @@ # You should have received a copy of the GNU Affero General Public License # along with Nassella. If not, see . -{$authelia_SUBDOMAIN}.{$ROOT_DOMAIN} { - import maintenance_intercept +#{$authelia_SUBDOMAIN}.{$ROOT_DOMAIN} { +# 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 -} diff --git a/all-apps/authelia/4/Makefile b/all-apps/authelia/4/Makefile index 6faac37..effe7fa 100644 --- a/all-apps/authelia/4/Makefile +++ b/all-apps/authelia/4/Makefile @@ -18,7 +18,8 @@ IMAGE_DEPS := $(IMAGE_DEPS) \ app/authelia/postgres_db \ app/authelia/postgres_user \ 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) 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/make-authelia-config.sh ./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) diff --git a/all-apps/authelia/4/authelia-config/configuration.yml.tmpl b/all-apps/authelia/4/authelia-config/configuration.yml.tmpl index 80cb9fe..e73ceeb 100644 --- a/all-apps/authelia/4/authelia-config/configuration.yml.tmpl +++ b/all-apps/authelia/4/authelia-config/configuration.yml.tmpl @@ -35,58 +35,45 @@ identity_validation: reset_password: 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: - ldap: - address: 'ldap://lldap:3890' - implementation: 'lldap' - timeout: '5s' - pooling: - enable: false - count: 5 - retries: 2 - timeout: '10 seconds' - base_dn: 'DC=nassella,DC=org' -# additional_users_dn: 'OU=users' -# additional_groups_dn: 'OU=groups' -# group_search_mode: 'filter' -# permit_referrals: false - permit_unauthenticated_bind: false - permit_feature_detection_failure: false - user: 'uid=admin,ou=people,dc=nassella,dc=org' - password: '$LLDAP_ADMIN_PASSWORD' - # attributes: - # distinguished_name: 'distinguishedName' - # username: 'uid' - # display_name: 'displayName' - # family_name: 'sn' - # given_name: 'givenName' - # middle_name: 'middleName' - # nickname: '' - # gender: '' - # birthdate: '' - # website: 'wWWHomePage' - # profile: '' - # picture: '' - # zoneinfo: '' - # locale: '' - # phone_number: 'telephoneNumber' - # phone_extension: '' - # street_address: 'streetAddress' - # locality: 'l' - # region: 'st' - # postal_code: 'postalCode' - # country: 'c' - # mail: 'mail' - # member_of: 'memberOf' - # group_name: 'cn' - # extra: - # extra_example: - # name: '' - # multi_valued: false - # value_type: 'string' + disable_reset_password: true + file: + path: '/config/users.yml' + watch: false + search: + email: false + case_insensitive: false + extra_attributes: + extra_example: + multi_valued: false + value_type: 'string' + password: + algorithm: 'argon2' + argon2: + variant: 'argon2id' + iterations: 3 + memory: 65536 + parallelism: 4 + key_length: 32 + salt_length: 16 + scrypt: + variant: 'scrypt' + iterations: 16 + block_size: 8 + parallelism: 1 + key_length: 32 + salt_length: 16 + pbkdf2: + variant: 'sha512' + iterations: 310000 + salt_length: 16 + sha2crypt: + variant: 'sha512' + iterations: 50000 + salt_length: 16 + bcrypt: + variant: 'standard' + cost: 12 access_control: default_policy: 'deny' @@ -102,11 +89,12 @@ session: secret: '$AUTHELIA_SESSION_SECRET' cookies: - - name: 'authelia_session' - domain: '$ROOT_DOMAIN' # Should match whatever your root protected domain is - authelia_url: 'https://$AUTHELIA_FULL_DOMAIN' + - name: 'dozzle_session' + domain: '$DOZZLE_FULL_DOMAIN' # Should match whatever your root protected domain is + authelia_url: 'https://$DOZZLE_FULL_DOMAIN/authelia' expiration: '1 hour' # 1 hour inactivity: '5 minutes' # 5 minutes + default_redirection_url: 'https://$DOZZLE_FULL_DOMAIN/' regulation: max_retries: 3 diff --git a/all-apps/authelia/4/docker-compose.yaml b/all-apps/authelia/4/docker-compose.yaml index e3b93fd..819a766 100644 --- a/all-apps/authelia/4/docker-compose.yaml +++ b/all-apps/authelia/4/docker-compose.yaml @@ -50,13 +50,11 @@ services: image: 'authelia/authelia' volumes: - ./authelia/authelia-config/configuration.yml:/config/configuration.yml:ro + - ./authelia/authelia-config/users.yml:/config/users.yml:ro networks: - lb - authelia_internal_db - - lldap_internal depends_on: - lldap: - condition: service_healthy authelia_db: condition: service_healthy restart: 'unless-stopped' @@ -66,9 +64,6 @@ services: networks: lb: - lldap_internal: - driver: bridge - internal: true authelia_internal_db: driver: bridge internal: true diff --git a/all-apps/dozzle/20/Caddyfile b/all-apps/dozzle/20/Caddyfile index a14c0db..3080b49 100644 --- a/all-apps/dozzle/20/Caddyfile +++ b/all-apps/dozzle/20/Caddyfile @@ -15,9 +15,15 @@ # along with Nassella. If not, see . {$dozzle_SUBDOMAIN}.{$ROOT_DOMAIN} { - forward_auth authelia:9091 { - uri /api/authz/forward-auth - copy_headers Remote-User Remote-Groups Remote-Email Remote-Name + 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 http://dozzle:8080 } - reverse_proxy http://dozzle:8080 } diff --git a/all-apps/nassella/b0.0.1/authelia-config/configuration.yml.tmpl b/all-apps/nassella/b0.0.1/authelia-config/configuration.yml.tmpl index bc3a7f9..ab35ebc 100644 --- a/all-apps/nassella/b0.0.1/authelia-config/configuration.yml.tmpl +++ b/all-apps/nassella/b0.0.1/authelia-config/configuration.yml.tmpl @@ -84,7 +84,7 @@ access_control: policy: 'two_factor' session: - secret: 'insecure_session_secret' + secret: 'insecure_session_secret' # TODO fix cookies: - name: 'authelia_session' diff --git a/src/Dockerfile b/src/Dockerfile index 9826dfe..cb55f43 100644 --- a/src/Dockerfile +++ b/src/Dockerfile @@ -45,6 +45,7 @@ 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 RUN apt-get update && apt-get -y --no-install-recommends install \ @@ -53,6 +54,7 @@ RUN apt-get update && apt-get -y --no-install-recommends install \ 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/ diff --git a/src/nassella.scm b/src/nassella.scm index 1f7d7e5..7037278 100644 --- a/src/nassella.scm +++ b/src/nassella.scm @@ -869,67 +869,37 @@ h1, h2, h3, h4, h5, h6 { `((success . #f) (result ,data)))))) -;; (define (test-backblaze-connection user-id instance-id key-id application-key bucket-url) -;; (let* ((password-path (conc "restic-password-" user-id "-" instance-id)) -;; (restic-password -;; (with-db/transaction -;; (lambda (db) -;; (get-instance-restic-password db user-id instance-id))))) -;; (dynamic-wind -;; (lambda () -;; (with-output-to-file password-path (lambda () (display restic-password)))) -;; (lambda () -;; ;; restic retries (indefinitely?) if the connection can't be made so -;; ;; we pass it through timeout to ensure it does not hang -;; (receive (in-port out-port pid err-port) -;; (cond-expand -;; (dev -;; (process* "timeout" `("--preserve-status" "8s" "docker" "run" "--rm" "--volume" -;; ,(conc (current-directory) "/" password-path ":/restic-password") -;; "-e" ,(conc "AWS_ACCESS_KEY_ID=" key-id) -;; "-e" ,(conc "AWS_SECRET_ACCESS_KEY=" application-key) -;; "-i" "restic/restic:0.18.0" "cat" "config" -;; "--repo" ,(conc "s3:" bucket-url) -;; "--password-file" "/restic-password" -;; "--json"))) -;; (else -;; (process* "/bin/timeout" -;; `("--preserve-status" "8s" "/bin/restic" "cat" "config" -;; "--repo" ,(conc "s3:" bucket-url) -;; "--password-file" ,(conc (current-directory) "/" password-path) -;; "--json") -;; `(("AWS_ACCESS_KEY_ID" . ,key-id) -;; ("AWS_SECRET_ACCESS_KEY" . ,application-key))))) -;; (let ((thread -;; (thread-start! -;; (lambda () -;; (let loop ((i 0)) -;; (thread-sleep! 1) -;; ;; We do a non-blocking wait here so that we don't -;; ;; block the entire web process. -;; (receive (wait-pid exit-normal status) (process-wait pid #t) -;; (if (= wait-pid 0) ;; wait-pid is 0 until the process has finished -;; (if (< i 12) ;; 12s timeout -;; (loop (+ i 1)) -;; `((success . #f) -;; (errors ((message . "timeout trying to connect to backblaze"))))) -;; (if exit-normal -;; (let ((res (with-input-from-port in-port read-json)) -;; (err (with-input-from-port err-port read-json))) -;; ;; status 10 is what is returned if we can connect but the repo -;; ;; is not initialized yet -;; (if (or res (and err (= status 10))) -;; `((success . #t) -;; (result . ,res)) -;; `((success . #f) -;; (errors ((message . "abnormal exit - check Key ID, Application ID, and Bucket URL")))))) -;; `((success . #f) -;; (errors ((message . "abnormal exit - check Key ID, Application ID, and Bucket URL")))) -;; )))))))) -;; (thread-join! thread)))) -;; (lambda () -;; (handle-exceptions exn 'ignore -;; (delete-file password-path)))))) +(define *instance-up-timeout* 1) ;; seconds +(define *instance-up-retry-time* 120) ;; 120s, 2 minutes +(define *instance-up-retries* (/ *instance-up-retry-time* *instance-up-timeout*)) +(define (instance-up? path) + (let loop ((i 0)) + (let ((thread + (thread-start! + (lambda () + (condition-case + (receive (data uri response) (with-input-from-request path #f read-string) + (if (eq? (response-code response) 401) + #t + (begin (thread-sleep! *instance-up-timeout*) ;; we depend on this taking at least as long as the timeout if the instance is still booting + #f))) + (e (exn client-error) + (if (eq? (response-code (get-condition-property e 'client-error 'response)) 401) + #t + (begin (thread-sleep! *instance-up-timeout*) ;; we depend on this taking at least as long as the timeout if the instance is still booting + #f))) + (e (exn net) + (thread-sleep! *instance-up-timeout*) + #f) + (e () + (thread-sleep! *instance-up-timeout*) + #f)))))) + (if (thread-join! thread *instance-up-timeout* #f) + #t + (if (< i *instance-up-retries*) + (loop (+ i 1)) + #f))))) + (define (deployment-directory user-id instance-id) (string-append "deploy-" (number->string user-id) "-" (->string instance-id))) @@ -1077,6 +1047,34 @@ chmod -R 777 /opt/keys"))) "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789") 64))) +(define (create-authelia-password-hash password) + ;; docker run --rm -it authelia/authelia:latest authelia crypto hash generate argon2 + (receive (in-port out-port pid err-port) + (cond-expand + (dev + (process* "docker" `("run" "--rm" "authelia/authelia:latest" "authelia" "crypto" "hash" "generate" "argon2" "--password" ,password))) + (else + (process* "/usr/local/bin/authelia" `("crypto" "hash" "generate" "argon2" "--password" ,password)))) + (let ((thread + (thread-start! + (lambda () + (let loop () + (thread-sleep! 1) + ;; We do a non-blocking wait here so that we don't + ;; block the entire web process. + (receive (wait-pid exit-normal status) (process-wait pid #t) + (if (= wait-pid 0) ;; wait-pid is 0 until the process has finished + (loop) + (if exit-normal + (begin + (with-input-from-port err-port read-string) ;; left here for debugging and to clear ports + ;; the output is prefixed with "Digest: " and suffixed with a newline + ;; so we clean it up + (string-drop (string-trim-right (with-input-from-port in-port read-string)) 8)) + (begin (log-to (debug-log) "authelia: docker command error") + (error "Authelia for generating password hash had abnormal exit")))))))))) + (thread-join! thread)))) + (define (generate-postgres-password) (generator->string (gtake (make-random-char-generator "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789") @@ -1401,8 +1399,9 @@ chmod -R 777 /opt/keys"))) '())) (Field (@ (name "wordpress") (type "checkbox") (label ("Wordpress")) (checked ,(member 'wordpress (alist-ref 'selected-apps results))))) (Field (@ (name "log-viewer") (type "checkbox") (label ("Log Viewer")) (checked #t) (disabled "disabled"))) - (Field (@ (name "lldap") (type "checkbox") (label ("Admin LLDAP")) (checked #t) (disabled "disabled"))) - (Field (@ (name "authelia") (type "checkbox") (label ("Admin Authelia")) (checked #t) (disabled "disabled")))) + ;; (Field (@ (name "lldap") (type "checkbox") (label ("Admin LLDAP")) (checked #t) (disabled "disabled"))) + ;; (Field (@ (name "authelia") (type "checkbox") (label ("Admin Authelia")) (checked #t) (disabled "disabled"))) + ) ;; TODO add config for when automatic upgrades are scheduled for? ;; TODO add config for server timezone? (Form-Nav (@ (back-to ,(conc "/config/wizard/services-success/" instance-id)))))))))) @@ -1424,7 +1423,7 @@ chmod -R 777 /opt/keys"))) (ghost . ,(or (and (alist-ref 'ghost (current-params)) "6") #f)) (nassella . ,(or (and (alist-ref 'nassella (current-params)) "b0.0.1") #f)) (wordpress . ,(or (and (alist-ref 'wordpress (current-params)) "8.4") #f)) - (lldap . "0.6.3") + (lldap . #f) ;; "0.6.3" (authelia . "4") (instance-control . "b0.0.1") (log-viewer . "20")))) @@ -1500,31 +1499,30 @@ chmod -R 777 /opt/keys"))) (Field (@ (name "wordpress-subdomain") (label ("Subdomain")) (required #t) (value ,(alist-ref 'subdomain (alist-ref 'wordpress app-config eq? '()) eq? "wordpress")))))) '()) - ,@(if (member 'lldap selected-apps) - `((Fieldset - (@ (title "Admin LLDAP")) - (Field (@ (name "lldap-subdomain") (label ("Subdomain")) (required #t) - (value ,(alist-ref 'subdomain (alist-ref 'lldap app-config eq? '()) eq? "lldap")))) - (Field (@ (name "lldap-user-email") (label ("Admin Email Address")) (type "email") (required #t) - (value ,(alist-ref 'user-email (alist-ref 'lldap app-config eq? '()) eq? "")))) - (Field (@ (name "lldap-admin-password") (label ("Admin Password")) (type "password") (required #t) - (value ,(alist-ref 'admin-password (alist-ref 'lldap app-config eq? '()) eq? "")))))) - '()) - ,@(if (member 'authelia selected-apps) - `((Fieldset - (@ (title "Admin Authelia")) - (Field (@ (name "authelia-subdomain") (label ("Subdomain")) (required #t) - (value ,(alist-ref 'subdomain (alist-ref 'authelia app-config eq? '()) eq? "authelia")))))) - '()) + ;; ,@(if (member 'lldap selected-apps) + ;; `((Fieldset + ;; (@ (title "Admin LLDAP")) + ;; (Field (@ (name "lldap-subdomain") (label ("Subdomain")) (required #t) + ;; (value ,(alist-ref 'subdomain (alist-ref 'lldap app-config eq? '()) eq? "lldap")))) + ;; (Field (@ (name "lldap-user-email") (label ("Admin Email Address")) (type "email") (required #t) + ;; (value ,(alist-ref 'user-email (alist-ref 'lldap app-config eq? '()) eq? "")))) + ;; (Field (@ (name "lldap-admin-password") (label ("Admin Password")) (type "password") (required #t) + ;; (value ,(alist-ref 'admin-password (alist-ref 'lldap app-config eq? '()) eq? "")))))) + ;; '()) + ;; ,@(if (member 'authelia selected-apps) + ;; `((Fieldset + ;; (@ (title "Admin Authelia")) + ;; (Field (@ (name "authelia-subdomain") (label ("Subdomain")) (required #t) + ;; (value ,(alist-ref 'subdomain (alist-ref 'authelia app-config eq? '()) eq? "authelia")))))) + ;; '()) (Fieldset (@ (title "Log Viewer")) (Field (@ (name "log-viewer-subdomain") (label ("Subdomain")) (required #t) (value ,(alist-ref 'subdomain (alist-ref 'log-viewer app-config eq? '()) eq? "logs")))) - ;; (Field (@ (name "log-viewer-user") (label ("Username")) (required #t) - ;; (value ,(alist-ref 'user (alist-ref 'log-viewer app-config eq? '()) eq? "")))) - ;; (Field (@ (name "log-viewer-password") (label ("Password")) (type "password") (required #t) - ;; (value ,(alist-ref 'password (alist-ref 'log-viewer app-config eq? '()) eq? "")))) - ) + (Field (@ (name "log-viewer-user") (label ("Admin Email")) (required #t) + (value ,(alist-ref 'user (alist-ref 'log-viewer app-config eq? '()) eq? "")))) + (Field (@ (name "log-viewer-password") (label ("Admin Password")) (type "password") (required #t) + (value ,(alist-ref 'password (alist-ref 'log-viewer app-config eq? '()) eq? ""))))) ,@(if (or (member 'nextcloud selected-apps) (member 'ghost selected-apps) (member 'nassella selected-apps) (member 'authelia selected-apps)) `((Fieldset (@ (title "All Apps - Email - SMTP")) @@ -1662,8 +1660,8 @@ chmod -R 777 /opt/keys"))) (alist-ref 'authelia config eq? '())) (generate-authelia-key-seed))))) (log-viewer . ((subdomain . ,(alist-ref 'log-viewer-subdomain (current-params))) - (user . "unused") ;; TODO can we remove? - (password . "unused"))) ;; TODO can we remove? + (user . ,(alist-ref 'log-viewer-user (current-params))) + (password . ,(alist-ref 'log-viewer-password (current-params))))) (all-apps . ((smtp-host . ,(alist-ref 'smtp-host (current-params))) (smtp-port . ,(alist-ref 'smtp-port (current-params))) (smtp-auth-user . ,(alist-ref 'smtp-auth-user (current-params))) @@ -1836,13 +1834,14 @@ chmod -R 777 /opt/keys"))) (map (lambda (app-version) (conc (if (eq? (car app-version) 'log-viewer) 'dozzle (car app-version)) "," - (alist-ref 'subdomain (alist-ref (car app-version) config)) + (or (alist-ref 'subdomain (alist-ref (car app-version) config)) + "nassella-ignore") ;; a hack to get authelia to work for now "," (cdr app-version))) selected-apps) " ")) ("HOST_ADMIN_USER" . ,(alist-ref 'user (alist-ref 'log-viewer config))) - ("HOST_ADMIN_PASSWORD" . ,(alist-ref 'password (alist-ref 'log-viewer config))) + ("HOST_ADMIN_PASSWORD" . ,(create-authelia-password-hash (alist-ref 'password (alist-ref 'log-viewer config)))) ("NEXTCLOUD_ADMIN_USER" . ,(alist-ref 'admin-user (alist-ref 'nextcloud config))) ("NEXTCLOUD_ADMIN_PASSWORD" . ,(alist-ref 'admin-password (alist-ref 'nextcloud config))) ("NEXTCLOUD_POSTGRES_DB" . "nextcloud") @@ -1999,13 +1998,29 @@ chmod -R 777 /opt/keys"))) (with-db/transaction (lambda (db) (update-deployment-progress db deployment-id progress) - (update-deployment-status - db user-id deployment-id - (if (= status 0) 'complete 'failed) - (with-input-from-file (string-append dir "/make-out") read-string)) (update-user-terraform-state db user-id instance-id (if (eof-object? tf-state) "" tf-state) - (if (eof-object? tf-state-backup) "" tf-state-backup)))))))))))))) + (if (eof-object? tf-state-backup) "" tf-state-backup)) + (if (= status 0) + (update-deployment-progress db deployment-id `((instance-up . in-progress))) + (update-deployment-status + db user-id deployment-id + 'failed + (with-input-from-file (string-append dir "/make-out") read-string))))) + (when (= status 0) + (let ((instance-up (instance-up? (string-append "https://" + (alist-ref 'subdomain (alist-ref 'log-viewer (alist-ref 'config app-config))) + "." + (alist-ref 'root-domain app-config))))) + (with-db/transaction + (lambda (db) + (update-deployment-progress db deployment-id `((instance-up . ,(if instance-up 'complete 'failed)))) + (update-deployment-status + db user-id deployment-id + (if instance-up + 'complete + 'failed) + (with-input-from-file (string-append dir "/make-out") read-string)))))))))))))))) (redirect (conc "/config/wizard/success/" (alist-ref "id" (current-params) equal?)))) (get/widgets @@ -2057,7 +2072,8 @@ chmod -R 777 /opt/keys"))) (ip-destroy . "Instance Mapped IP Disconnect") (ip-create . "Instance Mapped IP Connect") (volume-create . "App Volume Connect") - (machine-destroy . "Cleanup Previous Instance")))) + (machine-destroy . "Cleanup Previous Instance") + (instance-up . "Instance Starting Up")))) (form (@ (action "/dashboard") (method GET)) ,@(if (or (eq? (string->symbol status) 'complete) (eq? (string->symbol status) 'failed))