-
Notifications
You must be signed in to change notification settings - Fork 16
Expand file tree
/
Copy pathdeploy.sh
More file actions
executable file
·2398 lines (2119 loc) · 83.1 KB
/
deploy.sh
File metadata and controls
executable file
·2398 lines (2119 loc) · 83.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/bin/bash
# Automated Matrix Stack Deployment Script
# This script handles the complete deployment from scratch
# Supports both local testing and production deployments
set -e # Exit on error
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
MAGENTA='\033[0;35m'
NC='\033[0m' # No Color
# Use sudo for docker commands
DOCKER_CMD="sudo docker"
DOCKER_COMPOSE_CMD="sudo docker compose"
echo -e "${YELLOW}Using sudo for docker commands.${NC}"
echo ""
# Test docker access
if ! sudo docker ps &> /dev/null; then
echo -e "${RED}Error: Cannot access Docker. Please ensure Docker is running.${NC}"
exit 1
fi
clear
echo -e "${BLUE}╔════════════════════════════════════════════════════════════╗${NC}"
echo -e "${BLUE}║ Matrix Stack Automated Deployment Script ║${NC}"
echo -e "${BLUE}║ Interactive Setup ║${NC}"
echo -e "${BLUE}╚════════════════════════════════════════════════════════════╝${NC}"
echo ""
# ============================================================================
# DEPLOYMENT TYPE SELECTION
# ============================================================================
echo -e "${CYAN}Select Deployment Type:${NC}"
echo ""
echo -e " ${GREEN}1)${NC} Local Testing"
echo -e " → Everything on one machine with self-signed certificates"
echo -e " → Uses *.example.test domains (add to /etc/hosts)"
echo ""
echo -e " ${GREEN}2)${NC} Production (Single-Server)"
echo -e " → All services on one machine with Let's Encrypt certificates"
echo -e " → Real domains — point DNS to this machine"
echo -e " → Simplest production setup"
echo ""
echo -e " ${GREEN}3)${NC} Production (Distributed)"
echo -e " → Services on separate machines"
echo -e " → Caddy and Authelia run on dedicated hosts"
echo -e " → Generates config files to copy to each machine"
echo ""
read -p "Enter choice [1/2/3]: " DEPLOYMENT_TYPE
if [[ "$DEPLOYMENT_TYPE" == "1" ]]; then
DEPLOYMENT_MODE="local"
COMPOSE_FILE="compose-variants/docker-compose.local.yml"
# Docker Compose v5+ resolves volume paths relative to the compose file's directory.
# --project-directory . overrides this so all paths resolve from the project root.
DOCKER_COMPOSE_CMD="sudo docker compose --project-directory ."
echo -e "${GREEN}✓${NC} Selected: Local Testing Mode"
elif [[ "$DEPLOYMENT_TYPE" == "2" ]]; then
DEPLOYMENT_MODE="production-single"
COMPOSE_FILE="docker-compose.yml"
DOCKER_COMPOSE_CMD="sudo docker compose --project-directory ."
echo -e "${GREEN}✓${NC} Selected: Production (Single-Server)"
elif [[ "$DEPLOYMENT_TYPE" == "3" ]]; then
DEPLOYMENT_MODE="production"
COMPOSE_FILE="docker-compose.yml"
echo -e "${GREEN}✓${NC} Selected: Production (Distributed)"
else
echo -e "${RED}✗${NC} Invalid choice. Exiting."
exit 1
fi
echo ""
# ============================================================================
# DATA DIRECTORY CHECK & AUTOMATIC CLEANUP
# ============================================================================
echo -e "${YELLOW}Checking for existing data directories...${NC}"
EXISTING_DATA=""
PRESERVED_CLIENT_SECRET="" # Will store existing CLIENT_SECRET to preserve Authelia integration
[[ -d "postgres/data" ]] && [[ "$(ls -A postgres/data 2>/dev/null)" ]] && EXISTING_DATA="${EXISTING_DATA}postgres/data "
[[ -d "synapse/data" ]] && [[ -f "synapse/data/homeserver.yaml" ]] && EXISTING_DATA="${EXISTING_DATA}synapse/data "
[[ -d "mas/data" ]] && [[ "$(ls -A mas/data 2>/dev/null)" ]] && EXISTING_DATA="${EXISTING_DATA}mas/data "
if [[ -n "$EXISTING_DATA" ]]; then
echo -e "${RED}⚠ WARNING: Existing data directories found:${NC}"
for dir in $EXISTING_DATA; do
echo -e " • $dir"
done
echo ""
echo -e "${YELLOW}Automatically cleaning to prevent password mismatch issues...${NC}"
echo -e "${YELLOW}(Old database passwords won't match new deployment)${NC}"
echo ""
# Extract CLIENT_SECRET before cleanup to preserve Authelia integration
if [[ -f "mas/config/config.yaml" ]]; then
PRESERVED_CLIENT_SECRET=$(grep "client_secret:" mas/config/config.yaml | head -1 | sed "s/.*client_secret: '\(.*\)'/\1/")
if [[ -n "$PRESERVED_CLIENT_SECRET" ]]; then
echo -e "${GREEN}✓${NC} Found existing Authelia client_secret - will preserve it"
fi
fi
# Stop all containers and remove volumes to prevent PostgreSQL password conflicts
echo -e "${YELLOW}Stopping containers and removing volumes...${NC}"
$DOCKER_COMPOSE_CMD -f ${COMPOSE_FILE} down -v 2>/dev/null || true
# Surgical cleanup: Only remove postgres/data (source of password mismatch)
# Preserve synapse/data/homeserver.yaml (keeps custom mail config, etc.)
# Preserve mas/config (keeps CLIENT_SECRET for Authelia)
echo -e "${YELLOW}Cleaning PostgreSQL data to fix password mismatch...${NC}"
echo -e "${GREEN}✓${NC} Preserving synapse/data/homeserver.yaml (custom configs maintained)"
echo -e "${GREEN}✓${NC} Preserving mas/config (Authelia integration maintained)"
# Remove bridge registration references from homeserver.yaml
if [[ -f "synapse/data/homeserver.yaml" ]]; then
sudo sed -i '/^ - \/bridges\//d' synapse/data/homeserver.yaml
echo -e "${GREEN}✓${NC} Removed stale bridge registration references"
fi
sudo rm -rf postgres/data
sudo rm -rf mas/data mas/certs
sudo rm -rf caddy/data caddy/config
sudo rm -rf bridges/*/config
mkdir -p postgres/data synapse/data mas/data mas/certs caddy/data caddy/config
mkdir -p bridges/telegram/config bridges/whatsapp/config bridges/signal/config
echo -e "${GREEN}✓${NC} PostgreSQL data cleaned - custom configurations preserved"
echo ""
else
echo -e "${GREEN}✓${NC} No existing data found - starting with clean slate"
echo ""
fi
# ============================================================================
# SSO / OIDC PROVIDER SELECTION
# ============================================================================
echo -e "${CYAN}Upstream SSO / OIDC provider:${NC}"
echo ""
echo -e " ${GREEN}1)${NC} None — MAS handles authentication directly"
echo -e " → Simpler setup, password-based auth"
echo ""
echo -e " ${GREEN}2)${NC} Authelia — full SSO with 2FA"
echo -e " → Users authenticate through Authelia"
echo -e " → Additional auth layer with MFA policies"
echo ""
echo -e " ${GREEN}3)${NC} Other OIDC — Authentik, Keycloak, Zitadel, etc."
echo -e " → Any OIDC-compliant provider"
echo -e " → No extra containers required"
echo ""
read -p "Choose [1/2/3, default: 1]: " _sso_choice
USE_AUTHELIA=false
USE_CUSTOM_OIDC=false
OIDC_ISSUER_URL=""
OIDC_CLIENT_ID=""
OIDC_CLIENT_SECRET=""
case "${_sso_choice}" in
2)
USE_AUTHELIA=true
echo -e "${GREEN}✓${NC} Authelia SSO will be included"
;;
3)
USE_CUSTOM_OIDC=true
echo ""
read -p "OIDC issuer URL (e.g. https://auth.example.com/application/o/matrix/): " OIDC_ISSUER_URL
read -p "OIDC client ID: " OIDC_CLIENT_ID
read -s -p "OIDC client secret: " OIDC_CLIENT_SECRET
echo
echo -e "${GREEN}✓${NC} Custom OIDC provider configured"
;;
*)
echo -e "${GREEN}✓${NC} MAS will handle authentication directly"
;;
esac
echo ""
# ============================================================================
# ELEMENT CALL SELECTION
# ============================================================================
echo -e "${CYAN}Enable Element Call (video/voice calling)?${NC}"
echo ""
echo -e " ${GREEN}Yes)${NC} Add LiveKit-based video/voice calls"
echo -e " → Adds livekit + lk-jwt-service containers"
echo -e " → Element Web will show video/voice call button"
echo -e " → Requires ports TCP 7881 and UDP 50100-50200 open to the internet"
echo ""
echo -e " ${GREEN}No)${NC} Text-only Matrix stack (default)"
echo ""
read -p "Enable Element Call? [y/N]: " INCLUDE_ELEMENT_CALL
if [[ "$INCLUDE_ELEMENT_CALL" =~ ^[Yy]$ ]]; then
USE_ELEMENT_CALL=true
echo -e "${GREEN}✓${NC} Element Call will be enabled"
else
USE_ELEMENT_CALL=false
echo -e "${GREEN}✓${NC} Element Call disabled"
fi
echo ""
# ============================================================================
# OPEN REGISTRATION
# ============================================================================
if [[ "$USE_AUTHELIA" == true || "$USE_CUSTOM_OIDC" == true ]]; then
# SSO provider controls user provisioning — password registration prompt is not applicable
OPEN_REGISTRATION=false
else
echo -e "${CYAN}Allow open user registration?${NC}"
echo ""
echo -e " ${GREEN}Yes)${NC} Anyone can create an account via the login page"
echo -e " → Same experience as matrix.org"
echo -e " ${YELLOW}⚠${NC} Only enable if you intend a public server"
echo ""
echo -e " ${GREEN}No)${NC} Accounts must be created by an admin (default)"
echo -e " → Recommended for private/family servers"
echo ""
read -p "Allow open user registration? [y/N]: " reg_choice
if [[ "$reg_choice" =~ ^[Yy]$ ]]; then
OPEN_REGISTRATION=true
echo -e "${GREEN}✓${NC} Open registration enabled"
else
OPEN_REGISTRATION=false
echo -e "${GREEN}✓${NC} Registration restricted to admin-created accounts"
fi
fi
echo ""
# ============================================================================
# DOCKER REGISTRY AND HARDENED IMAGES
# ============================================================================
echo -e "${CYAN}Docker Image Configuration:${NC}"
echo ""
read -p "Custom Docker registry prefix (leave blank for default): " DOCKER_REGISTRY_INPUT
DOCKER_REGISTRY="${DOCKER_REGISTRY_INPUT%/}" # strip trailing slash
[ -n "$DOCKER_REGISTRY" ] && DOCKER_REGISTRY="${DOCKER_REGISTRY}/"
if [ -n "$DOCKER_REGISTRY" ]; then
echo -e "${GREEN}✓${NC} Custom registry: ${DOCKER_REGISTRY}"
else
echo -e "${GREEN}✓${NC} Using default registries"
fi
USE_HARDENED_IMAGES=false
read -p "Use hardened images from dhi.io for Redis/PostgreSQL/Caddy? [y/N]: " yn
[[ "$yn" =~ ^[Yy] ]] && USE_HARDENED_IMAGES=true
if [ "$USE_HARDENED_IMAGES" = true ]; then
echo -e "${GREEN}✓${NC} Hardened images (dhi.io) enabled for Redis/PostgreSQL/Caddy"
else
echo -e "${GREEN}✓${NC} Using standard images"
fi
echo ""
# Build image reference helper
build_image() {
local image="$1"
if [ -n "$DOCKER_REGISTRY" ]; then
echo "${DOCKER_REGISTRY}${image}"
else
echo "${image}"
fi
}
# Standard images (respect custom registry)
POSTGRES_IMAGE=$(build_image "postgres:16-alpine")
SYNAPSE_IMAGE=$(build_image "matrixdotorg/synapse:latest")
ELEMENT_IMAGE=$(build_image "vectorim/element-web:latest")
ELEMENT_ADMIN_IMAGE=$(build_image "oci.element.io/element-admin:latest")
MAS_IMAGE=$(build_image "ghcr.io/element-hq/matrix-authentication-service:latest")
TELEGRAM_IMAGE=$(build_image "dock.mau.dev/mautrix/telegram:latest")
WHATSAPP_IMAGE=$(build_image "dock.mau.dev/mautrix/whatsapp:latest")
SIGNAL_IMAGE=$(build_image "dock.mau.dev/mautrix/signal:latest")
LIVEKIT_IMAGE=$(build_image "livekit/livekit-server:latest")
LK_JWT_IMAGE=$(build_image "ghcr.io/element-hq/lk-jwt-service:latest")
ELEMENT_CALL_IMAGE=$(build_image "ghcr.io/element-hq/element-call:latest")
AUTHELIA_IMAGE=$(build_image "authelia/authelia:latest")
# Hardened images take priority for redis/postgres/caddy
if [ "$USE_HARDENED_IMAGES" = true ]; then
REDIS_IMAGE="dhi.io/redis:7"
POSTGRES_IMAGE="dhi.io/postgres:16"
CADDY_IMAGE="dhi.io/caddy:2"
else
REDIS_IMAGE=$(build_image "redis:7-alpine")
CADDY_IMAGE=$(build_image "caddy:2-alpine")
fi
# Function to generate secure random string (32 bytes base64)
generate_secret() {
openssl rand -base64 32 | tr -d "=+/" | cut -c1-32
}
# Function to generate secure hex string (for MAS encryption)
generate_hex_secret() {
openssl rand -hex 32
}
# Function to print status
print_status() {
echo -e "${GREEN}✓${NC} $1"
}
print_error() {
echo -e "${RED}✗${NC} $1"
}
print_warning() {
echo -e "${YELLOW}⚠${NC} $1"
}
print_info() {
echo -e "${BLUE}ℹ${NC} $1"
}
# ============================================================================
# DOMAIN AND CONFIGURATION PROMPTS
# ============================================================================
if [[ "$DEPLOYMENT_MODE" == "local" ]]; then
# Local testing with example.test domains (not .localhost - it's on the public suffix list!)
DOMAIN_BASE="example.test"
AUTHELIA_COOKIE_DOMAIN="example.test" # No leading dot for cookie domain
MATRIX_DOMAIN="matrix.example.test"
ELEMENT_DOMAIN="element.example.test"
ADMIN_DOMAIN="admin.example.test"
AUTH_DOMAIN="auth.example.test"
AUTHELIA_DOMAIN="authelia.example.test"
RTC_DOMAIN="rtc.example.test"
CALL_DOMAIN="call.example.test"
# Matrix server name (MXID identity domain)
echo ""
echo -e "${CYAN}Matrix User ID format:${NC}"
echo -e " [1] Short: @user:${DOMAIN_BASE} ← recommended"
echo -e " [2] Subdomain: @user:${MATRIX_DOMAIN}"
read -p "Choose [1/2, default: 1]: " _sn_choice
if [[ "$_sn_choice" == "2" ]]; then
SERVER_NAME="${MATRIX_DOMAIN}"
else
SERVER_NAME="${DOMAIN_BASE}"
fi
echo -e "${CYAN}Local Testing Configuration:${NC}"
echo -e " Matrix API: https://${MATRIX_DOMAIN}"
echo -e " Element Web: https://${ELEMENT_DOMAIN}"
echo -e " MAS Auth: https://${AUTH_DOMAIN}"
echo -e " Authelia: https://${AUTHELIA_DOMAIN}"
if [[ "$USE_ELEMENT_CALL" == true ]]; then
echo -e " Element Call: https://${CALL_DOMAIN}"
echo -e " LiveKit RTC: https://${RTC_DOMAIN}"
fi
echo ""
HOSTS_DOMAINS="${MATRIX_DOMAIN} ${ELEMENT_DOMAIN} ${AUTH_DOMAIN} ${AUTHELIA_DOMAIN}"
if [[ "$SERVER_NAME" != "$MATRIX_DOMAIN" ]]; then
HOSTS_DOMAINS="${SERVER_NAME} ${HOSTS_DOMAINS}"
fi
if [[ "$USE_ELEMENT_CALL" == true ]]; then
HOSTS_DOMAINS="${HOSTS_DOMAINS} ${RTC_DOMAIN} ${CALL_DOMAIN}"
fi
echo -e "${YELLOW}⚠ Remember to add these to /etc/hosts:${NC}"
echo -e " 127.0.0.1 ${HOSTS_DOMAINS}"
echo -e " ::1 ${HOSTS_DOMAINS}"
echo ""
echo -e "${BLUE}ℹ Note: IPv6 entry (::1) required to prevent DNS lookups bypassing /etc/hosts${NC}"
echo ""
read -p "Press Enter to continue..."
echo ""
else
# Production deployment (single-server or distributed)
echo -e "${CYAN}Production Deployment Configuration${NC}"
echo ""
# Base domain
read -p "Enter your base domain (e.g., example.com): " DOMAIN_BASE
AUTHELIA_COOKIE_DOMAIN="${DOMAIN_BASE}"
# Matrix subdomain
read -p "Enter Matrix subdomain [default: matrix]: " MATRIX_SUBDOMAIN
MATRIX_SUBDOMAIN=${MATRIX_SUBDOMAIN:-matrix}
MATRIX_DOMAIN="${MATRIX_SUBDOMAIN}.${DOMAIN_BASE}"
# Element subdomain
read -p "Enter Element subdomain [default: element]: " ELEMENT_SUBDOMAIN
ELEMENT_SUBDOMAIN=${ELEMENT_SUBDOMAIN:-element}
ELEMENT_DOMAIN="${ELEMENT_SUBDOMAIN}.${DOMAIN_BASE}"
# Element Admin subdomain
read -p "Enter Element Admin subdomain [default: admin]: " ADMIN_SUBDOMAIN
ADMIN_SUBDOMAIN=${ADMIN_SUBDOMAIN:-admin}
ADMIN_DOMAIN="${ADMIN_SUBDOMAIN}.${DOMAIN_BASE}"
# MAS subdomain
read -p "Enter MAS/Auth subdomain [default: auth]: " AUTH_SUBDOMAIN
AUTH_SUBDOMAIN=${AUTH_SUBDOMAIN:-auth}
AUTH_DOMAIN="${AUTH_SUBDOMAIN}.${DOMAIN_BASE}"
# Authelia subdomain
read -p "Enter Authelia subdomain [default: authelia]: " AUTHELIA_SUBDOMAIN
AUTHELIA_SUBDOMAIN=${AUTHELIA_SUBDOMAIN:-authelia}
AUTHELIA_DOMAIN="${AUTHELIA_SUBDOMAIN}.${DOMAIN_BASE}"
# RTC subdomain (LiveKit signaling) + Call subdomain (Element Call frontend)
if [[ "$USE_ELEMENT_CALL" == true ]]; then
read -p "Enter RTC subdomain for LiveKit [default: rtc]: " RTC_SUBDOMAIN
RTC_SUBDOMAIN=${RTC_SUBDOMAIN:-rtc}
RTC_DOMAIN="${RTC_SUBDOMAIN}.${DOMAIN_BASE}"
read -p "Enter subdomain for Element Call frontend [default: call]: " CALL_SUBDOMAIN
CALL_SUBDOMAIN=${CALL_SUBDOMAIN:-call}
CALL_DOMAIN="${CALL_SUBDOMAIN}.${DOMAIN_BASE}"
fi
# Matrix server name (MXID identity domain)
echo ""
echo -e "${CYAN}Matrix User ID format:${NC}"
echo -e " [1] Short: @user:${DOMAIN_BASE} ← recommended"
echo -e " [2] Subdomain: @user:${MATRIX_DOMAIN}"
read -p "Choose [1/2, default: 1]: " _sn_choice
if [[ "$_sn_choice" == "2" ]]; then
SERVER_NAME="${MATRIX_DOMAIN}"
else
SERVER_NAME="${DOMAIN_BASE}"
fi
if [[ "$DEPLOYMENT_MODE" == "production" ]]; then
echo ""
echo -e "${CYAN}Backend Server Addresses (for Caddyfile):${NC}"
echo -e " ${YELLOW}Enter IP addresses or hostnames${NC}"
echo ""
# Matrix server address (IP or hostname)
read -p "Matrix server address (IP or hostname): " MATRIX_SERVER_IP
MATRIX_SERVER_IP=${MATRIX_SERVER_IP:-10.0.1.10}
# Authelia server address (IP or hostname)
read -p "Authelia server address (IP or hostname): " AUTHELIA_SERVER_IP
AUTHELIA_SERVER_IP=${AUTHELIA_SERVER_IP:-10.0.1.20}
fi
# Email for Let's Encrypt (both production modes)
read -p "Email for Let's Encrypt [default: admin@${DOMAIN_BASE}]: " LETSENCRYPT_EMAIL
LETSENCRYPT_EMAIL=${LETSENCRYPT_EMAIL:-admin@${DOMAIN_BASE}}
echo ""
echo -e "${GREEN}✓${NC} Configuration Summary:"
echo -e " Base Domain: ${DOMAIN_BASE}"
echo -e " Server Name: ${SERVER_NAME} (@user:${SERVER_NAME})"
echo -e " Matrix: https://${MATRIX_DOMAIN}"
echo -e " Element: https://${ELEMENT_DOMAIN}"
echo -e " MAS: https://${AUTH_DOMAIN}"
if [[ "$DEPLOYMENT_MODE" == "production" ]]; then
echo -e " Authelia: https://${AUTHELIA_DOMAIN}"
fi
if [[ "$USE_ELEMENT_CALL" == true ]]; then
echo -e " Element Call: https://${CALL_DOMAIN}"
echo -e " LiveKit RTC: https://${RTC_DOMAIN}"
fi
if [[ "$DEPLOYMENT_MODE" == "production" ]]; then
echo -e " Matrix Backend: ${MATRIX_SERVER_IP}"
echo -e " Authelia Backend: ${AUTHELIA_SERVER_IP}"
print_info "Note: Generated Caddyfile will use these backend addresses"
print_info " Copy generated configs from authelia/config/ to your Authelia server"
fi
echo ""
fi
# Step 1: Check prerequisites
echo -e "${BLUE}[1/13] Checking prerequisites...${NC}"
if ! command -v openssl &> /dev/null; then
print_error "openssl is not installed"
exit 1
fi
if ! $DOCKER_CMD --version &> /dev/null; then
print_error "Docker is not accessible"
exit 1
fi
print_status "Prerequisites OK"
echo ""
# Step 1.5: Create directory structure
echo -e "${BLUE}[1.5/13] Creating directory structure...${NC}"
mkdir -p authelia/config
mkdir -p mas/config mas/data mas/certs
mkdir -p element/config
mkdir -p synapse/data
mkdir -p postgres/data
mkdir -p caddy/data caddy/config
mkdir -p bridges/{telegram,whatsapp,signal}/config
mkdir -p appservices
print_status "Directory structure created"
echo ""
# Step 1.5 (cont.): Ensure livekit directory exists with correct ownership
mkdir -p livekit
sudo chown "$(id -u):$(id -g)" livekit 2>/dev/null || true
# Step 2: Generate secure secrets
echo -e "${BLUE}[2/12] Generating secure secrets...${NC}"
POSTGRES_PASSWORD=$(generate_secret)
AUTHELIA_JWT_SECRET=$(generate_secret)
AUTHELIA_SESSION_SECRET=$(generate_secret)
AUTHELIA_STORAGE_ENCRYPTION_KEY=$(generate_secret)
MAS_SECRET_KEY=$(generate_hex_secret) # MAS requires hex format
SYNAPSE_SHARED_SECRET=$(generate_secret)
DOUBLEPUPPET_AS_TOKEN=$(generate_hex_secret)
DOUBLEPUPPET_HS_TOKEN=$(generate_hex_secret)
if [[ "$USE_ELEMENT_CALL" == true ]]; then
LIVEKIT_SECRET=$(generate_hex_secret)
fi
print_status "Secrets generated"
echo ""
# Step 3: Update .env file
echo -e "${BLUE}[3/13] Updating .env file with generated secrets...${NC}"
cat > .env << EOF
# Matrix Stack Environment Variables
# Auto-generated by deploy.sh on $(date)
# Deployment Mode: ${DEPLOYMENT_MODE}
# Domain Configuration
DOMAIN_BASE=${DOMAIN_BASE}
MATRIX_DOMAIN=${MATRIX_DOMAIN}
ELEMENT_DOMAIN=${ELEMENT_DOMAIN}
ADMIN_DOMAIN=${ADMIN_DOMAIN}
AUTH_DOMAIN=${AUTH_DOMAIN}
AUTHELIA_DOMAIN=${AUTHELIA_DOMAIN}
SERVER_NAME=${SERVER_NAME}
# PostgreSQL
POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
# Synapse
SYNAPSE_REPORT_STATS=no
SYNAPSE_SHARED_SECRET=${SYNAPSE_SHARED_SECRET}
# Authelia
AUTHELIA_JWT_SECRET=${AUTHELIA_JWT_SECRET}
AUTHELIA_SESSION_SECRET=${AUTHELIA_SESSION_SECRET}
AUTHELIA_STORAGE_ENCRYPTION_KEY=${AUTHELIA_STORAGE_ENCRYPTION_KEY}
AUTHELIA_POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
# MAS
MAS_DATABASE_URL=postgresql://synapse:${POSTGRES_PASSWORD}@postgres/mas
MAS_SECRET_KEY=${MAS_SECRET_KEY}
# Timezone
TZ=${TZ:-Europe/Berlin}
# Docker images
POSTGRES_IMAGE=${POSTGRES_IMAGE}
SYNAPSE_IMAGE=${SYNAPSE_IMAGE}
ELEMENT_IMAGE=${ELEMENT_IMAGE}
ELEMENT_ADMIN_IMAGE=${ELEMENT_ADMIN_IMAGE}
REDIS_IMAGE=${REDIS_IMAGE}
MAS_IMAGE=${MAS_IMAGE}
TELEGRAM_IMAGE=${TELEGRAM_IMAGE}
WHATSAPP_IMAGE=${WHATSAPP_IMAGE}
SIGNAL_IMAGE=${SIGNAL_IMAGE}
LIVEKIT_IMAGE=${LIVEKIT_IMAGE}
LK_JWT_IMAGE=${LK_JWT_IMAGE}
ELEMENT_CALL_IMAGE=${ELEMENT_CALL_IMAGE}
AUTHELIA_IMAGE=${AUTHELIA_IMAGE}
CADDY_IMAGE=${CADDY_IMAGE}
EOF
# Add production-specific variables
if [[ "$DEPLOYMENT_MODE" == "production" ]]; then
cat >> .env << EOF
# Production Configuration (Backend addresses for Caddyfile generation)
# These are used in the generated caddy/Caddyfile.production template
MATRIX_SERVER_IP=${MATRIX_SERVER_IP}
AUTHELIA_SERVER_IP=${AUTHELIA_SERVER_IP}
LETSENCRYPT_EMAIL=${LETSENCRYPT_EMAIL}
EOF
fi
# Add Element Call variables
if [[ "$USE_ELEMENT_CALL" == true ]]; then
cat >> .env << EOF
# Element Call (LiveKit + self-hosted frontend)
RTC_DOMAIN=${RTC_DOMAIN}
CALL_DOMAIN=${CALL_DOMAIN}
LIVEKIT_SECRET=${LIVEKIT_SECRET}
EOF
fi
# Add custom OIDC variables
if [[ "$USE_CUSTOM_OIDC" == true ]]; then
cat >> .env << EOF
# Custom OIDC provider (configured via deploy.sh)
OIDC_ISSUER_URL=${OIDC_ISSUER_URL}
OIDC_CLIENT_ID=${OIDC_CLIENT_ID}
OIDC_CLIENT_SECRET=${OIDC_CLIENT_SECRET}
EOF
fi
# Telegram bridge credentials placeholder (obtain from https://my.telegram.org)
if ! grep -q "TELEGRAM_API_ID" .env 2>/dev/null; then
cat >> .env << 'ENVEOF'
# Telegram Bridge — obtain API credentials at https://my.telegram.org (Apps tab)
# Uncomment and fill in before running setup-bridges.sh to enable Telegram
# TELEGRAM_API_ID=your_api_id
# TELEGRAM_API_HASH=your_api_hash
ENVEOF
fi
print_status ".env file updated"
echo ""
# Conditional: Authelia configuration (Steps 4-8)
if [[ "$USE_AUTHELIA" == true ]]; then
# Step 4: Generate RSA key for Authelia
echo -e "${BLUE}[4/13] Generating RSA key for Authelia OIDC...${NC}"
openssl genrsa -out authelia_private.pem 4096 2>/dev/null
AUTHELIA_RSA_KEY=$(cat authelia_private.pem)
print_status "Authelia RSA key generated"
echo ""
# Step 5: Generate or reuse client secret for Authelia
echo -e "${BLUE}[5/13] Configuring Authelia client secret...${NC}"
if [[ -n "$PRESERVED_CLIENT_SECRET" ]]; then
# Reuse preserved secret to maintain Authelia integration
CLIENT_SECRET_PLAIN="$PRESERVED_CLIENT_SECRET"
CLIENT_SECRET_HASH=$($DOCKER_CMD run --rm authelia/authelia:latest authelia crypto hash generate pbkdf2 --variant sha512 --password "${CLIENT_SECRET_PLAIN}" 2>/dev/null | grep "Digest:" | awk '{print $2}')
print_status "Reusing preserved client secret (Authelia integration maintained)"
else
# Generate new secret
CLIENT_SECRET_PLAIN=$(generate_secret)
CLIENT_SECRET_HASH=$($DOCKER_CMD run --rm authelia/authelia:latest authelia crypto hash generate pbkdf2 --variant sha512 --password "${CLIENT_SECRET_PLAIN}" 2>/dev/null | grep "Digest:" | awk '{print $2}')
print_status "Client secret generated"
fi
echo ""
# Step 6: Generate password hash for default admin user
echo -e "${BLUE}[6/13] Generating default admin user...${NC}"
ADMIN_PASSWORD=$(generate_secret) # Generate secure random password
ADMIN_PASSWORD_HASH=$($DOCKER_CMD run --rm authelia/authelia:latest authelia crypto hash generate argon2 --password "${ADMIN_PASSWORD}" 2>/dev/null | grep "Digest:" | awk '{print $2}')
print_status "Admin user password hash generated"
print_warning "Default admin password: ${ADMIN_PASSWORD} (SAVE THIS - you'll need it to log in!)"
echo ""
# Step 7: Update Authelia configuration
echo -e "${BLUE}[7/12] Configuring Authelia...${NC}"
cat > authelia/config/configuration.yml << EOF
---
# Authelia Configuration for Matrix Stack
theme: auto
server:
address: 'tcp://0.0.0.0:9091'
log:
level: 'info'
format: 'text'
authentication_backend:
file:
path: '/config/users_database.yml'
password:
algorithm: 'argon2'
argon2:
variant: 'argon2id'
iterations: 3
memory: 65536
parallelism: 4
key_length: 32
salt_length: 16
session:
secret: '${AUTHELIA_SESSION_SECRET}'
cookies:
- domain: '${AUTHELIA_COOKIE_DOMAIN}'
authelia_url: 'https://${AUTHELIA_DOMAIN}'
default_redirection_url: 'https://${ELEMENT_DOMAIN}'
redis:
host: 'redis'
port: 6379
storage:
encryption_key: '${AUTHELIA_STORAGE_ENCRYPTION_KEY}'
postgres:
address: 'tcp://postgres:5432'
database: 'authelia'
username: 'synapse'
password: '${POSTGRES_PASSWORD}'
notifier:
filesystem:
filename: '/config/notification.txt'
identity_validation:
reset_password:
jwt_secret: '${AUTHELIA_JWT_SECRET}'
access_control:
default_policy: 'deny'
rules:
- domain:
- 'matrix.localhost'
policy: 'two_factor'
- domain:
- 'element.matrix.localhost'
policy: 'two_factor'
identity_providers:
oidc:
hmac_secret: '${AUTHELIA_JWT_SECRET}'
jwks:
- key_id: 'main'
algorithm: 'RS256'
use: 'sig'
key: |
EOF
# Add the RSA key with proper indentation
echo "$AUTHELIA_RSA_KEY" | sed 's/^/ /' >> authelia/config/configuration.yml
# Continue with the rest of the config
cat >> authelia/config/configuration.yml << EOF
clients:
- client_id: 'mas-client'
client_name: 'Matrix Authentication Service'
client_secret: '${CLIENT_SECRET_HASH}'
public: false
authorization_policy: 'one_factor' # Change to two_factor in production!
redirect_uris:
- 'https://${AUTH_DOMAIN}/callback'
- 'https://${AUTH_DOMAIN}/oauth2/callback'
- 'https://${AUTH_DOMAIN}/upstream/callback/01HQW90Z35CMXFJWQPHC3BGZGQ' # MAS upstream callback
scopes:
- 'openid'
- 'profile'
- 'email'
- 'offline_access'
grant_types:
- 'authorization_code'
- 'refresh_token'
response_types:
- 'code'
token_endpoint_auth_method: 'client_secret_basic'
EOF
print_status "Authelia configuration updated"
echo ""
# Step 8: Create Authelia users database
echo -e "${BLUE}[8/13] Creating Authelia users database...${NC}"
cat > authelia/config/users_database.yml << EOF
---
# Authelia Users Database
users:
admin:
displayname: "Admin User"
password: "${ADMIN_PASSWORD_HASH}"
email: admin@${MATRIX_DOMAIN}
groups:
- admins
- users
EOF
print_status "Authelia users database created"
echo ""
else
print_info "Skipping Authelia configuration (not included in deployment)"
echo ""
fi
# Step 9: Generate MAS signing key and Synapse client secret
echo -e "${BLUE}[9/12] Generating MAS signing key and Synapse client secret...${NC}"
openssl genrsa 4096 2>/dev/null | openssl pkcs8 -topk8 -nocrypt > mas-signing.key 2>/dev/null
MAS_SIGNING_KEY=$(cat mas-signing.key)
SYNAPSE_CLIENT_SECRET=$(generate_secret)
print_status "MAS signing key and Synapse client secret generated"
echo ""
# Step 10: Configure MAS
echo -e "${BLUE}[10/12] Configuring MAS...${NC}"
cat > mas/config/config.yaml << EOF
---
# Matrix Authentication Service (MAS) Configuration
http:
listeners:
- name: web
resources:
- name: discovery
- name: human
- name: oauth
- name: compat
- name: graphql
playground: true
- name: assets # Required for CSS/JS files
- name: adminapi
binds:
- address: '[::]:8080'
- name: internal
resources:
- name: health
binds:
- address: '127.0.0.1:8081'
public_base: 'https://${AUTH_DOMAIN}/'
issuer: 'https://${AUTH_DOMAIN}/'
database:
uri: 'postgresql://synapse:${POSTGRES_PASSWORD}@postgres/mas'
auto_migrate: true
secrets:
encryption: '${MAS_SECRET_KEY}'
keys:
- kid: 'key-1'
algorithm: rs256
key: |
EOF
# Add the MAS signing key with proper indentation
echo "$MAS_SIGNING_KEY" | sed 's/^/ /' >> mas/config/config.yaml
# Continue with the rest of the MAS config - conditional based on Authelia usage
if [[ "$USE_AUTHELIA" == true ]]; then
# With Authelia: Use upstream OAuth2 provider
# Set discovery URL based on deployment mode
if [[ "$DEPLOYMENT_MODE" == "production" ]]; then
AUTHELIA_DISCOVERY_URL="https://${AUTHELIA_DOMAIN}/.well-known/openid-configuration"
else
# Local: Use internal HTTP to avoid self-signed cert issues between containers
AUTHELIA_DISCOVERY_URL="http://authelia:9091/.well-known/openid-configuration"
fi
cat >> mas/config/config.yaml << EOF
upstream_oauth2:
providers:
- id: '01HQW90Z35CMXFJWQPHC3BGZGQ'
issuer: 'https://${AUTHELIA_DOMAIN}'
discovery_url: '${AUTHELIA_DISCOVERY_URL}'
client_id: 'mas-client'
client_secret: '${CLIENT_SECRET_PLAIN}'
scope: 'openid profile email offline_access'
token_endpoint_auth_method: 'client_secret_basic'
fetch_userinfo: true # Critical: Must fetch userinfo for Authelia claims
claims_imports:
localpart:
action: force
template: '{{ user.preferred_username }}' # Works with Authelia
displayname:
action: suggest
template: '{{ user.preferred_username }}' # Authelia provides preferred_username, not name
email:
action: force
template: '{{ user.email }}'
set_email_verification: always
matrix:
homeserver: '${SERVER_NAME}'
endpoint: 'http://synapse:8008'
secret: '${SYNAPSE_SHARED_SECRET}'
passwords:
enabled: false
account:
password_registration_enabled: false
password_change_allowed: true
password_recovery_enabled: false
account_deactivation_allowed: true
EOF
elif [[ "$USE_CUSTOM_OIDC" == true ]]; then
# With custom OIDC: Use external upstream OAuth2 provider
cat >> mas/config/config.yaml << EOF
upstream_oauth2:
providers:
- id: '01HQW90Z35CMXFJWQPHC3BGZGQ'
issuer: '${OIDC_ISSUER_URL}'
client_id: '${OIDC_CLIENT_ID}'
client_secret: '${OIDC_CLIENT_SECRET}'
scope: 'openid profile email offline_access'
token_endpoint_auth_method: 'client_secret_basic'
fetch_userinfo: true
claims_imports:
localpart:
action: force
template: '{{ user.preferred_username }}'
displayname:
action: suggest
template: '{{ user.name | default(user.preferred_username) }}'
email:
action: force
template: '{{ user.email }}'
set_email_verification: always
matrix:
homeserver: '${SERVER_NAME}'
endpoint: 'http://synapse:8008'
secret: '${SYNAPSE_SHARED_SECRET}'
passwords:
enabled: false
account:
password_registration_enabled: false
password_change_allowed: true
password_recovery_enabled: false
account_deactivation_allowed: true
EOF
else
# Without SSO: MAS handles authentication directly
cat >> mas/config/config.yaml << EOF
matrix:
homeserver: '${SERVER_NAME}'
endpoint: 'http://synapse:8008'
secret: '${SYNAPSE_SHARED_SECRET}'
passwords:
enabled: true
minimum_complexity: 3
schemes:
- version: 1
algorithm: argon2id
account:
password_registration_enabled: ${OPEN_REGISTRATION}
password_registration_email_required: false
password_change_allowed: true
password_recovery_enabled: false
account_deactivation_allowed: true
EOF
fi
# Common configuration continues (email, branding, policy, clients)
cat >> mas/config/config.yaml << EOF
email:
from: '"Matrix Authentication Service" <noreply@matrix.localhost>'
reply_to: '"Matrix Support" <support@matrix.localhost>'
transport: smtp
hostname: 'localhost'
port: 25
mode: plain
branding:
service_name: 'Matrix'
policy_uri: 'https://${AUTH_DOMAIN}/privacy'
tos_uri: 'https://${AUTH_DOMAIN}/terms'
policy:
data:
registration:
enabled: ${OPEN_REGISTRATION}
clients:
# Element Web client (public)
- client_id: '01HQW90Z35CMXFJWQPHC3BGZGQ'
client_auth_method: none
redirect_uris:
- 'https://${ELEMENT_DOMAIN}'
- 'https://${ELEMENT_DOMAIN}/mobile_guide/'
- 'io.element.app:/callback'
# Element Admin (public - for admin UI)
- client_id: '01ADMN00000000000000000000'
client_auth_method: none
redirect_uris:
- 'https://${ADMIN_DOMAIN}/'
- 'https://${ADMIN_DOMAIN}'
# Synapse client (confidential - for backend integration)
- client_id: '0000000000000000000SYNAPSE'
client_auth_method: client_secret_basic
client_secret: '${SYNAPSE_CLIENT_SECRET}'
EOF
if [[ "$USE_AUTHELIA" == true ]]; then
print_status "MAS configuration created (with Authelia upstream provider)"
elif [[ "$USE_CUSTOM_OIDC" == true ]]; then
print_status "MAS configuration created (with custom OIDC provider: ${OIDC_ISSUER_URL})"
else
print_status "MAS configuration created (password authentication enabled)"
fi
echo ""
# Step 11: Create Element Web configuration
echo -e "${BLUE}[11/13] Creating Element Web configuration...${NC}"
# Build Element Call block if enabled