1 { config, lib, pkgs, ... }:
6 cfg = config.services.jitsi-meet;
8 # The configuration files are JS of format "var <<string>> = <<JSON>>;". In order to
9 # override only some settings, we need to extract the JSON, use jq to merge it with
10 # the config provided by user, and then reconstruct the file.
12 source: varName: userCfg: appendExtra:
14 extractor = pkgs.writeText "extractor.js" ''
15 var fs = require("fs");
16 eval(fs.readFileSync(process.argv[2], 'utf8'));
17 process.stdout.write(JSON.stringify(eval(process.argv[3])));
19 userJson = pkgs.writeText "user.json" (builtins.toJSON userCfg);
20 in (pkgs.runCommand "${varName}.js" { } ''
21 ${pkgs.nodejs}/bin/node ${extractor} ${source} ${varName} > default.json
23 echo "var ${varName} = "
24 ${pkgs.jq}/bin/jq -s '.[0] * .[1]' default.json ${userJson}
26 echo ${escapeShellArg appendExtra}
30 # Essential config - it's probably not good to have these as option default because
31 # types.attrs doesn't do merging. Let's merge explicitly, can still be overridden if
35 domain = cfg.hostName;
36 muc = "conference.${cfg.hostName}";
37 focus = "focus.${cfg.hostName}";
38 jigasi = "jigasi.${cfg.hostName}";
40 bosh = "//${cfg.hostName}/http-bind";
41 websocket = "wss://${cfg.hostName}/xmpp-websocket";
43 fileRecordingsEnabled = true;
44 liveStreamingEnabled = true;
45 hiddenDomain = "recorder.${cfg.hostName}";
49 options.services.jitsi-meet = with types; {
50 enable = mkEnableOption "Jitsi Meet - Secure, Simple and Scalable Video Conferences";
54 example = "meet.example.org";
56 FQDN of the Jitsi Meet instance.
63 example = literalExpression ''
65 enableWelcomePage = false;
70 Client-side web application settings that override the defaults in {file}`config.js`.
72 See <https://github.com/jitsi/jitsi-meet/blob/master/config.js> for default
73 configuration with comments.
77 extraConfig = mkOption {
81 Text to append to {file}`config.js` web application config file.
83 Can be used to insert JavaScript logic to determine user's region in cascading bridges setup.
87 interfaceConfig = mkOption {
90 example = literalExpression ''
92 SHOW_JITSI_WATERMARK = false;
93 SHOW_WATERMARK_FOR_GUESTS = false;
97 Client-side web-app interface settings that override the defaults in {file}`interface_config.js`.
99 See <https://github.com/jitsi/jitsi-meet/blob/master/interface_config.js> for
100 default configuration with comments.
109 Jitsi Videobridge instance and configure it to connect to Prosody.
111 Additional configuration is possible with {option}`services.jitsi-videobridge`
115 passwordFile = mkOption {
118 example = "/run/keys/videobridge";
120 File containing password to the Prosody account for videobridge.
122 If `null`, a file with password will be generated automatically. Setting
123 this option is useful if you plan to connect additional videobridges to the XMPP server.
128 jicofo.enable = mkOption {
132 Whether to enable JiCoFo instance and configure it to connect to Prosody.
134 Additional configuration is possible with {option}`services.jicofo`.
138 jibri.enable = mkOption {
142 Whether to enable a Jibri instance and configure it to connect to Prosody.
144 Additional configuration is possible with {option}`services.jibri`, and
145 {option}`services.jibri.finalizeScript` is especially useful.
149 jigasi.enable = mkOption {
153 Whether to enable jigasi instance and configure it to connect to Prosody.
155 Additional configuration is possible with <option>services.jigasi</option>.
159 nginx.enable = mkOption {
163 Whether to enable nginx virtual host that will serve the javascript application and act as
164 a proxy for the XMPP server. Further nginx configuration can be done by adapting
165 {option}`services.nginx.virtualHosts.<hostName>`.
166 When this is enabled, ACME will be used to retrieve a TLS certificate by default. To disable
167 this, set the {option}`services.nginx.virtualHosts.<hostName>.enableACME` to
168 `false` and if appropriate do the same for
169 {option}`services.nginx.virtualHosts.<hostName>.forceSSL`.
173 caddy.enable = mkEnableOption "caddy reverse proxy to expose jitsi-meet";
175 prosody.enable = mkOption {
180 Whether to configure Prosody to relay XMPP messages between Jitsi Meet components. Turn this
181 off if you want to configure it manually.
184 prosody.lockdown = mkOption {
189 Whether to disable Prosody features not needed by Jitsi Meet.
191 The default Prosody configuration assumes that it will be used as a
192 general-purpose XMPP server rather than as a companion service for
193 Jitsi Meet. This option reconfigures Prosody to only listen on
194 localhost without support for TLS termination, XMPP federation or
195 the file transfer proxy.
199 excalidraw.enable = mkEnableOption "Excalidraw collaboration backend for Jitsi";
200 excalidraw.port = mkOption {
203 description = ''The port which the Excalidraw backend for Jitsi should listen to.'';
207 enable = mkEnableOption "Authenticated room creation";
208 authentication = mkOption {
210 default = "internal_hashed";
211 description = ''The authentication type to be used by jitsi'';
216 config = mkIf cfg.enable {
217 services.prosody = mkIf cfg.prosody.enable {
218 enable = mkDefault true;
219 xmppComplianceSuite = mkDefault false;
221 admin_adhoc = mkDefault false;
222 bosh = mkDefault true;
223 ping = mkDefault true;
224 roster = mkDefault true;
225 saslauth = mkDefault true;
226 smacks = mkDefault true;
227 tls = mkDefault true;
228 websocket = mkDefault true;
229 proxy65 = mkIf cfg.prosody.lockdown (mkDefault false);
231 httpInterfaces = mkIf cfg.prosody.lockdown (mkDefault [ "127.0.0.1" ]);
232 httpsPorts = mkIf cfg.prosody.lockdown (mkDefault []);
235 domain = "conference.${cfg.hostName}";
236 name = "Jitsi Meet MUC";
238 roomDefaultPublicJids = true;
240 restrict_room_creation = true
242 admins = { "focus@auth.${cfg.hostName}" }
246 domain = "breakout.${cfg.hostName}";
247 name = "Jitsi Meet Breakout MUC";
249 roomDefaultPublicJids = true;
251 restrict_room_creation = true
253 admins = { "focus@auth.${cfg.hostName}", "jvb@auth.${cfg.hostName}" }
257 domain = "internal.auth.${cfg.hostName}";
258 name = "Jitsi Meet Videobridge MUC";
260 roomDefaultPublicJids = true;
263 admins = { "focus@auth.${cfg.hostName}", "jvb@auth.${cfg.hostName}", "jigasi@auth.${cfg.hostName}" }
265 #-- muc_room_cache_size = 1000
268 domain = "lobby.${cfg.hostName}";
269 name = "Jitsi Meet Lobby MUC";
271 roomDefaultPublicJids = true;
273 restrict_room_creation = true
283 "conference_duration"
296 extraPluginPaths = [ "${pkgs.jitsi-meet-prosody}/share/prosody-plugins" ];
297 extraConfig = lib.mkMerge [
299 Component "focus.${cfg.hostName}" "client_proxy"
300 target_address = "focus@auth.${cfg.hostName}"
302 Component "jigasi.${cfg.hostName}" "client_proxy"
303 target_address = "jigasi@auth.${cfg.hostName}"
305 Component "speakerstats.${cfg.hostName}" "speakerstats_component"
306 muc_component = "conference.${cfg.hostName}"
308 Component "conferenceduration.${cfg.hostName}" "conference_duration_component"
309 muc_component = "conference.${cfg.hostName}"
311 Component "endconference.${cfg.hostName}" "end_conference"
312 muc_component = "conference.${cfg.hostName}"
314 Component "avmoderation.${cfg.hostName}" "av_moderation_component"
315 muc_component = "conference.${cfg.hostName}"
317 Component "metadata.${cfg.hostName}" "room_metadata_component"
318 muc_component = "conference.${cfg.hostName}"
319 breakout_rooms_component = "breakout.${cfg.hostName}"
322 muc_mapper_domain_base = "${cfg.hostName}"
324 cross_domain_websocket = true;
325 consider_websocket_secure = true;
328 "focus@auth.${cfg.hostName}",
329 "jvb@auth.${cfg.hostName}"
331 '' + optionalString cfg.prosody.lockdown ''
332 c2s_interfaces = { "127.0.0.1" };
333 modules_disabled = { "s2s" };
336 virtualHosts.${cfg.hostName} = {
338 domain = cfg.hostName;
340 authentication = ${if cfg.secureDomain.enable then "\"${cfg.secureDomain.authentication}\"" else "\"jitsi-anonymous\""}
341 c2s_require_encryption = false
342 admins = { "focus@auth.${cfg.hostName}" }
343 smacks_max_unacked_stanzas = 5
344 smacks_hibernation_time = 60
345 smacks_max_hibernated_sessions = 1
346 smacks_max_old_sessions = 1
348 av_moderation_component = "avmoderation.${cfg.hostName}"
349 speakerstats_component = "speakerstats.${cfg.hostName}"
350 conference_duration_component = "conferenceduration.${cfg.hostName}"
351 end_conference_component = "endconference.${cfg.hostName}"
353 c2s_require_encryption = false
354 lobby_muc = "lobby.${cfg.hostName}"
355 breakout_rooms_muc = "breakout.${cfg.hostName}"
356 room_metadata_component = "metadata.${cfg.hostName}"
357 main_muc = "conference.${cfg.hostName}"
360 cert = "/var/lib/jitsi-meet/jitsi-meet.crt";
361 key = "/var/lib/jitsi-meet/jitsi-meet.key";
364 virtualHosts."auth.${cfg.hostName}" = {
366 domain = "auth.${cfg.hostName}";
368 authentication = "internal_hashed"
371 cert = "/var/lib/jitsi-meet/jitsi-meet.crt";
372 key = "/var/lib/jitsi-meet/jitsi-meet.key";
375 virtualHosts."recorder.${cfg.hostName}" = {
377 domain = "recorder.${cfg.hostName}";
379 authentication = "internal_plain"
380 c2s_require_encryption = false
383 virtualHosts."guest.${cfg.hostName}" = {
385 domain = "guest.${cfg.hostName}";
387 authentication = "anonymous"
388 c2s_require_encryption = false
392 systemd.services.prosody = mkIf cfg.prosody.enable {
394 videobridgeSecret = if cfg.videobridge.passwordFile != null then cfg.videobridge.passwordFile else "/var/lib/jitsi-meet/videobridge-secret";
396 ${config.services.prosody.package}/bin/prosodyctl register focus auth.${cfg.hostName} "$(cat /var/lib/jitsi-meet/jicofo-user-secret)"
397 ${config.services.prosody.package}/bin/prosodyctl register jvb auth.${cfg.hostName} "$(cat ${videobridgeSecret})"
398 ${config.services.prosody.package}/bin/prosodyctl mod_roster_command subscribe focus.${cfg.hostName} focus@auth.${cfg.hostName}
399 ${config.services.prosody.package}/bin/prosodyctl register jibri auth.${cfg.hostName} "$(cat /var/lib/jitsi-meet/jibri-auth-secret)"
400 ${config.services.prosody.package}/bin/prosodyctl register recorder recorder.${cfg.hostName} "$(cat /var/lib/jitsi-meet/jibri-recorder-secret)"
401 '' + optionalString cfg.jigasi.enable ''
402 ${config.services.prosody.package}/bin/prosodyctl register jigasi auth.${cfg.hostName} "$(cat /var/lib/jitsi-meet/jigasi-user-secret)"
406 EnvironmentFile = [ "/var/lib/jitsi-meet/secrets-env" ];
407 SupplementaryGroups = [ "jitsi-meet" ];
409 reloadIfChanged = true;
412 users.groups.jitsi-meet = { };
413 systemd.tmpfiles.rules = [
414 "d '/var/lib/jitsi-meet' 0750 root jitsi-meet - -"
417 systemd.services.jitsi-meet-init-secrets = {
418 wantedBy = [ "multi-user.target" ];
419 before = [ "jicofo.service" "jitsi-videobridge2.service" ] ++ (optional cfg.prosody.enable "prosody.service") ++ (optional cfg.jigasi.enable "jigasi.service");
424 Group = "jitsi-meet";
425 WorkingDirectory = "/var/lib/jitsi-meet";
429 secrets = [ "jicofo-component-secret" "jicofo-user-secret" "jibri-auth-secret" "jibri-recorder-secret" ] ++ (optionals cfg.jigasi.enable [ "jigasi-user-secret" "jigasi-component-secret" ]) ++ (optional (cfg.videobridge.passwordFile == null) "videobridge-secret");
432 ${concatMapStringsSep "\n" (s: ''
433 if [ ! -f ${s} ]; then
434 tr -dc a-zA-Z0-9 </dev/urandom | head -c 64 > ${s}
438 # for easy access in prosody
439 echo "JICOFO_COMPONENT_SECRET=$(cat jicofo-component-secret)" > secrets-env
440 echo "JIGASI_COMPONENT_SECRET=$(cat jigasi-component-secret)" >> secrets-env
442 + optionalString cfg.prosody.enable ''
443 # generate self-signed certificates
444 if [ ! -f /var/lib/jitsi-meet/jitsi-meet.crt ]; then
445 ${getBin pkgs.openssl}/bin/openssl req \
448 -keyout /var/lib/jitsi-meet/jitsi-meet.key \
449 -out /var/lib/jitsi-meet/jitsi-meet.crt \
452 -subj '/CN=${cfg.hostName}/CN=auth.${cfg.hostName}'
453 chmod 640 /var/lib/jitsi-meet/jitsi-meet.key
458 systemd.services.jitsi-excalidraw = mkIf cfg.excalidraw.enable {
459 description = "Excalidraw collaboration backend for Jitsi";
460 after = [ "network.target" ];
461 wantedBy = [ "multi-user.target" ];
462 environment.PORT = toString cfg.excalidraw.port;
466 ExecStart = "${pkgs.jitsi-excalidraw}/bin/jitsi-excalidraw-backend";
467 Restart = "on-failure";
470 Group = "jitsi-meet";
471 CapabilityBoundingSet = "";
472 NoNewPrivileges = true;
473 ProtectSystem = "strict";
477 ProtectKernelLogs = true;
479 PrivateDevices = true;
481 ProtectHostname = true;
482 ProtectKernelTunables = true;
483 ProtectKernelModules = true;
484 ProtectControlGroups = true;
485 RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
486 RestrictNamespaces = true;
487 LockPersonality = true;
488 RestrictRealtime = true;
489 RestrictSUIDSGID = true;
490 SystemCallFilter = [ "@system-service @pkey" "~@privileged" ];
494 services.nginx = mkIf cfg.nginx.enable {
495 enable = mkDefault true;
496 virtualHosts.${cfg.hostName} = {
497 enableACME = mkDefault true;
498 forceSSL = mkDefault true;
499 root = pkgs.jitsi-meet;
503 locations."@root_path".extraConfig = ''
504 rewrite ^/(.*)$ / break;
506 locations."~ ^/([^/\\?&:'\"]+)$".tryFiles = "$uri @root_path";
507 locations."^~ /xmpp-websocket" = {
509 proxyPass = "http://localhost:5280/xmpp-websocket";
510 proxyWebsockets = true;
512 locations."=/http-bind" = {
513 proxyPass = "http://localhost:5280/http-bind";
515 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
516 proxy_set_header Host $host;
519 locations."=/external_api.js" = mkDefault {
520 alias = "${pkgs.jitsi-meet}/libs/external_api.min.js";
522 locations."=/_api/room-info" = {
523 proxyPass = "http://localhost:5280/room-info";
525 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
526 proxy_set_header Host $host;
529 locations."=/config.js" = mkDefault {
530 alias = overrideJs "${pkgs.jitsi-meet}/config.js" "config" (recursiveUpdate defaultCfg cfg.config) cfg.extraConfig;
532 locations."=/interface_config.js" = mkDefault {
533 alias = overrideJs "${pkgs.jitsi-meet}/interface_config.js" "interfaceConfig" cfg.interfaceConfig "";
535 locations."/socket.io/" = mkIf cfg.excalidraw.enable {
536 proxyPass = "http://127.0.0.1:${toString cfg.excalidraw.port}";
537 proxyWebsockets = true;
542 services.caddy = mkIf cfg.caddy.enable {
543 enable = mkDefault true;
544 virtualHosts.${cfg.hostName} = {
547 templatedJitsiMeet = pkgs.runCommand "templated-jitsi-meet" { } ''
548 cp -R --no-preserve=all ${pkgs.jitsi-meet}/* .
549 for file in *.html **/*.html ; do
550 ${pkgs.sd}/bin/sd '<!--#include virtual="(.*)" -->' '{{ include "$1" }}' $file
553 rm interface_config.js
555 cp ${overrideJs "${pkgs.jitsi-meet}/config.js" "config" (recursiveUpdate defaultCfg cfg.config) cfg.extraConfig} $out/config.js
556 cp ${overrideJs "${pkgs.jitsi-meet}/interface_config.js" "interfaceConfig" cfg.interfaceConfig ""} $out/interface_config.js
557 cp ./libs/external_api.min.js $out/external_api.js
559 in (optionalString cfg.excalidraw.enable ''
561 reverse_proxy 127.0.0.1:${toString cfg.excalidraw.port}
565 header Host ${cfg.hostName}
566 reverse_proxy 127.0.0.1:5280
568 handle /xmpp-websocket {
569 reverse_proxy 127.0.0.1:5280
573 root * ${templatedJitsiMeet}
574 try_files {path} {path}
575 try_files {path} /index.html
582 services.jitsi-meet.config = recursiveUpdate
583 (mkIf cfg.excalidraw.enable {
586 collabServerBaseUrl = "https://${cfg.hostName}";
589 (mkIf cfg.secureDomain.enable {
590 hosts.anonymousdomain = "guest.${cfg.hostName}";
593 services.jitsi-videobridge = mkIf cfg.videobridge.enable {
595 xmppConfigs."localhost" = {
597 domain = "auth.${cfg.hostName}";
598 passwordFile = "/var/lib/jitsi-meet/videobridge-secret";
599 mucJids = "jvbbrewery@internal.auth.${cfg.hostName}";
600 disableCertificateVerification = true;
604 services.jicofo = mkIf cfg.jicofo.enable {
606 xmppHost = "localhost";
607 xmppDomain = cfg.hostName;
608 userDomain = "auth.${cfg.hostName}";
610 userPasswordFile = "/var/lib/jitsi-meet/jicofo-user-secret";
611 componentPasswordFile = "/var/lib/jitsi-meet/jicofo-component-secret";
612 bridgeMuc = "jvbbrewery@internal.auth.${cfg.hostName}";
614 jicofo.xmpp.service.disable-certificate-verification = true;
615 jicofo.xmpp.client.disable-certificate-verification = true;
617 (lib.mkIf (config.services.jibri.enable || cfg.jibri.enable) {
619 brewery-jid = "JibriBrewery@internal.auth.${cfg.hostName}";
620 pending-timeout = "90";
623 (lib.mkIf cfg.secureDomain.enable {
628 login-url = cfg.hostName;
630 xmpp.client.client-proxy = "focus.${cfg.hostName}";
635 services.jibri = mkIf cfg.jibri.enable {
638 xmppEnvironments."jitsi-meet" = {
639 xmppServerHosts = [ "localhost" ];
640 xmppDomain = cfg.hostName;
643 domain = "internal.auth.${cfg.hostName}";
644 roomName = "JibriBrewery";
649 domain = "auth.${cfg.hostName}";
651 passwordFile = "/var/lib/jitsi-meet/jibri-auth-secret";
655 domain = "recorder.${cfg.hostName}";
656 username = "recorder";
657 passwordFile = "/var/lib/jitsi-meet/jibri-recorder-secret";
661 disableCertificateVerification = true;
662 stripFromRoomDomain = "conference.";
666 services.jigasi = mkIf cfg.jigasi.enable {
668 xmppHost = "localhost";
669 xmppDomain = cfg.hostName;
670 userDomain = "auth.${cfg.hostName}";
672 userPasswordFile = "/var/lib/jitsi-meet/jigasi-user-secret";
673 componentPasswordFile = "/var/lib/jitsi-meet/jigasi-component-secret";
674 bridgeMuc = "jigasibrewery@internal.${cfg.hostName}";
676 "org.jitsi.jigasi.ALWAYS_TRUST_MODE_ENABLED" = "true";
681 meta.doc = ./jitsi-meet.md;
682 meta.maintainers = lib.teams.jitsi.members;