diff --git a/applications/bitwarden/_bitwarden-directory-connector.nix b/applications/bitwarden/_bitwarden-directory-connector.nix new file mode 100644 index 0000000..55edf6d --- /dev/null +++ b/applications/bitwarden/_bitwarden-directory-connector.nix @@ -0,0 +1,52 @@ +{ + lib, + buildNpmPackage, + fetchgit, + pkgs, + git, + python3, + pkg-config, + libsecret, + nodejs_18, +}: let + buildNpmPackage' = buildNpmPackage.override {nodejs = nodejs_18;}; +in + buildNpmPackage' rec { + pname = "bitwarden-directory-connector"; + version = "v2023.10.0"; + + src = fetchgit { + url = "https://github.com/bitwarden/directory-connector.git"; + rev = version; + hash = "sha256-5gU7nIPHU94Yhd83C9y0ABL9PbSfMn9WhV2wlpdr2fE="; + }; + + npmDepsHash = "sha256-jBAWWY12qeX2EDhUvT3TQpnQvYXRsIilRrXGpVzxYvw="; + + env.ELECTRON_SKIP_BINARY_DOWNLOAD = "1"; + + makeCacheWritable = true; + npmBuildScript = "build:cli:prod"; + + installPhase = '' + mkdir -p $out + cp -R {build-cli,node_modules} $out + ''; + + buildInputs = [ + libsecret + ]; + + nativeBuildInputs = [ + git + python3 + pkg-config + ]; + + meta = with lib; { + description = "Bitwarden Directory Connector"; + homepage = "https://github.com/bitwarden/directory-connector"; + license = licenses.gpl3Only; + maintainers = with maintainers; [Silver-Golden]; + }; + } diff --git a/applications/bitwarden/_bitwarden_sync_module.nix b/applications/bitwarden/_bitwarden_sync_module.nix new file mode 100644 index 0000000..3ca5157 --- /dev/null +++ b/applications/bitwarden/_bitwarden_sync_module.nix @@ -0,0 +1,334 @@ +{ + pkgs, + config, + lib, + ... +}: +with lib; let + # to be changed once the package is accepted + connector = pkgs.callPackage ./_bitwarden-directory-connector.nix {}; + + cfg = config.services.bitwarden_connector; + + nodejs = pkgs.nodejs-18_x; + + ldap_data = '' + { + "ssl": ${boolToString cfg.ldap.ssl}, + "startTls": ${boolToString cfg.ldap.startTls}, + "sslAllowUnauthorized": ${boolToString cfg.ldap.startTls}, + "port": ${toString cfg.ldap.port}, + "currentUser": false, + "ad": ${boolToString cfg.ldap.ad}, + "pagedSearch": true, + "password": "to_be_replaced", + "hostname": "${cfg.ldap.hostname}", + "rootPath": "${cfg.ldap.root}", + "username": "${cfg.ldap.username}" + } + ''; + + sync_data = '' + { + "removeDisabled": ${boolToString cfg.sync.removeDisabled}, + "overwriteExisting": ${boolToString cfg.sync.overwriteExisting}, + "largeImport": ${boolToString cfg.sync.largeImport}, + "creationDateAttribute": "${cfg.sync.creationDateAttribute}", + "memberAttribute": "${cfg.sync.memberAttribute}", + + "useEmailPrefixSuffix": ${boolToString cfg.sync.emailPrefixSuffix.enable}, + ${optionalString cfg.sync.emailPrefixSuffix.enable '' + "emailPrefixAttribute": "${cfg.sync.emailPrefixSuffix.prefixAttribute}", + "emailSuffix": "${cfg.sync.emailPrefixSuffix.suffix}", + ''} + + "users": ${boolToString cfg.sync.users.enable}, + ${optionalString cfg.sync.users.enable '' + "userPath": "${cfg.sync.users.path}", + "userObjectClass": "${cfg.sync.users.objectClass}", + "userEmailAttribute": "${cfg.sync.users.emailAttribute}", + "userFilter": "${cfg.sync.users.filter}", + ''} + + "groups": ${boolToString cfg.sync.groups.enable}, + ${optionalString cfg.sync.groups.enable '' + "groupPath": "${cfg.sync.groups.path}", + "groupObjectClass": "${cfg.sync.groups.objectClass}", + "groupNameAttribute": "${cfg.sync.groups.nameAttribute}", + "groupFilter": "${cfg.sync.groups.filter}", + ''} + + "interval": 5 + } + ''; + + sed_string = string: builtins.replaceStrings ["." "/" "\n"] ["\\." "\\/" "\\n"] string; +in { + imports = []; + + options.services.bitwarden_connector = { + enable = mkEnableOption "Bitwarden Directory Connector"; + + domain = mkOption { + type = types.str; + description = lib.mdDoc "The domain the Bitwarden/Vaultwarden is accessable on."; + example = "https://vaultwarden.example.com"; + }; + + user = mkOption { + type = types.str; + description = lib.mdDoc "User to run the program."; + default = "bwdc"; + }; + + directory = mkOption { + type = types.str; + description = lib.mdDoc "Folder to store the config file."; + default = "/etc/bitwarden/${cfg.user}"; + }; + + ldap = { + ssl = mkOption { + type = types.bool; + default = false; + description = lib.mdDoc "Use SSL."; + }; + startTls = mkOption { + type = types.bool; + default = false; + description = lib.mdDoc "Use startTls."; + }; + sslAllowUnauthorized = mkOption { + type = types.bool; + default = false; + description = lib.mdDoc ""; + }; + ad = mkOption { + type = types.bool; + default = false; + description = lib.mdDoc "Is Active Directory."; + }; + port = mkOption { + type = types.int; + default = 389; + description = lib.mdDoc "Port LDAP is accessable on"; + }; + hostname = mkOption { + type = types.str; + description = lib.mdDoc "The host the LDAP is accessable on."; + example = "ldap.example.com"; + }; + + root = mkOption { + type = types.str; + description = lib.mdDoc "Root path for LDAP"; + example = "dc=example,dc=com"; + }; + + username = mkOption { + type = types.str; + description = lib.mdDoc "The user to authenticate as."; + example = "cn=admin,dc=example,dc=com"; + }; + pw_env = mkOption { + type = types.str; + description = lib.mdDoc "The ENV var that the ldap password is stored."; + default = "LDAP_PW"; + }; + }; + + sync = { + interval = mkOption { + type = types.str; + default = "*:0,15,30,45"; + description = lib.mdDoc "When to run the connector, cron syntax."; + }; + removeDisabled = mkOption { + type = types.bool; + default = true; + description = lib.mdDoc "Remove users from bitwarden groups if no longer in the ldap group."; + }; + overwriteExisting = mkOption { + type = types.bool; + default = false; + description = + lib.mdDoc "Remove and re-add users/groups, See https://bitwarden.com/help/user-group-filters/#overwriting-syncs for more details."; + }; + largeImport = mkOption { + type = types.bool; + default = false; + description = lib.mdDoc "Enable if you ar syncing more than 2000 users/groups."; + }; + + memberAttribute = mkOption { + type = types.str; + description = lib.mdDoc "Attribute that lists members in a LDAP group."; + example = "uniqueMember"; + }; + + creationDateAttribute = mkOption { + type = types.str; + description = lib.mdDoc "Attribute that lists a users creation date."; + example = "whenCreated"; + }; + + emailPrefixSuffix = { + enable = mkOption { + type = types.bool; + default = false; + description = lib.mdDoc "If a user has no email address, combine a username prefix with a suffix value to form an email."; + }; + prefixAttribute = mkOption { + type = types.str; + description = lib.mdDoc "Attribute that has a users username."; + example = "accountName"; + }; + suffix = mkOption { + type = types.str; + description = lib.mdDoc "Suffix for the email, normally @example.com."; + example = "@example.com"; + }; + }; + + users = { + enable = mkOption { + type = types.bool; + default = false; + description = lib.mdDoc "Sync users."; + }; + path = mkOption { + type = types.str; + description = lib.mdDoc "User directory, relative to root."; + example = "ou=users"; + }; + objectClass = mkOption { + type = types.str; + description = lib.mdDoc "A class that users will have."; + example = "inetOrgPerson"; + }; + emailAttribute = mkOption { + type = types.str; + description = lib.mdDoc "Attribute for a users email."; + example = "mail"; + }; + filter = mkOption { + type = types.str; + description = lib.mdDoc "Filter for users."; + example = "(memberOf=cn=sales,ou=groups,dc=example,dc=com)"; + }; + }; + groups = { + enable = mkOption { + type = types.bool; + default = false; + description = lib.mdDoc "Sync groups."; + }; + path = mkOption { + type = types.str; + description = lib.mdDoc "Group directory, relative to root."; + example = "ou=groups"; + }; + objectClass = mkOption { + type = types.str; + description = lib.mdDoc "A class that groups will have."; + example = "groupOfNames"; + }; + nameAttribute = mkOption { + type = types.str; + description = lib.mdDoc "Attribute for a name of group."; + example = "cn"; + }; + filter = mkOption { + type = types.str; + description = lib.mdDoc "Filter for groups."; + example = "(cn=sales)"; + }; + }; + }; + + env = { + description = "Env files to be passed in."; + ldap = mkOption rec { + type = types.str; + description = "Auth for the LDAP, has ${cfg.ldap.pw_env}"; + }; + bitwarden = mkOption rec { + type = types.str; + description = "Auth for Bitwarden, has BW_CLIENTID and BW_CLIENTSECRET"; + }; + }; + }; + + config = mkIf cfg.enable { + users.groups."${cfg.user}" = {}; + + users.users."${cfg.user}" = { + createHome = true; + isSystemUser = true; + home = "${cfg.directory}"; + group = "${cfg.user}"; + homeMode = "711"; + }; + + systemd = { + timers."${cfg.user}" = { + description = "Timer for ${cfg.user}"; + wantedBy = ["timers.target"]; + partOf = ["${cfg.user}.service"]; + timerConfig = { + OnCalendar = cfg.sync.interval; + Unit = "${cfg.user}.service"; + Persistent = true; + }; + }; + + services."${cfg.user}" = { + description = "Main process for Bitwarden Directory Connector"; + wantedBy = ["multi-user.target"]; + after = ["network-online.target"]; + wants = []; + + environment = { + BITWARDENCLI_CONNECTOR_APPDATA_DIR = cfg.directory; + BITWARDENCLI_CONNECTOR_PLAINTEXT_SECRETS = "true"; + }; + + serviceConfig = { + Type = "oneshot"; + User = "${cfg.user}"; + Group = "${cfg.user}"; + ExecStartPre = pkgs.writeShellScript "${cfg.user}-config" '' + # create the config file + ${nodejs}/bin/node ${connector}/build-cli/bwdc.js data-file + + ${nodejs}/bin/node ${connector}/build-cli/bwdc.js config server ${cfg.domain} + + # now login to set credentials + ${nodejs}/bin/node ${connector}/build-cli/bwdc.js login + + # set the ldap details + sed -i 's/"ldap": null/"ldap": ${sed_string ldap_data}/' ${cfg.directory}/data.json + + # set the client id + orgID=$(echo $BW_CLIENTID | sed 's/organization\.//g') + sed -i "s/\"organizationId\": null/\"organizationId\": \"$orgID\"/" ${cfg.directory}/data.json + + # and sync data + sed -i 's/"sync": null/"sync": ${sed_string sync_data}/' ${cfg.directory}/data.json + + # final config + ${nodejs}/bin/node ${connector}/build-cli/bwdc.js config directory 0 + ${nodejs}/bin/node ${connector}/build-cli/bwdc.js config ldap.password --secretenv ${cfg.ldap.pw_env} + ''; + + ExecStart = ''${nodejs}/bin/node ${connector}/build-cli/bwdc.js sync''; + + EnvironmentFile = [ + "${cfg.env.ldap}" + "${cfg.env.bitwarden}" + ]; + }; + }; + }; + }; +} diff --git a/applications/bitwarden/bitwarden_sync.nix b/applications/bitwarden/bitwarden_sync.nix new file mode 100644 index 0000000..a993846 --- /dev/null +++ b/applications/bitwarden/bitwarden_sync.nix @@ -0,0 +1,64 @@ +{ + pkgs, + config, + lib, + ... +}: let +in { + imports = [ + ./_bitwarden_sync_module.nix + ]; + + options = {}; + + config = { + age.secrets.bitwarden_sync_api.file = ../../secrets/bitwarden/api.age; + age.secrets.bitwarden_sync_ldap.file = ../../secrets/ldap/details.age; + + services.bitwarden_connector = { + enable = true; + + domain = "https://pw.skynet.ie"; + + ldap = { + ssl = false; + startTls = false; + sslAllowUnauthorized = false; + ad = false; + port = 389; + hostname = "account.skynet.ie"; + root = "dc=skynet,dc=ie"; + username = "cn=admin,dc=skynet,dc=ie"; + pw_env = "LDAP_ADMIN_PW"; + }; + + sync = { + removeDisabled = true; + overwriteExisting = false; + largeImport = false; + memberAttribute = "member"; + creationDateAttribute = "skCreated"; + emailPrefixSuffix.enable = false; + users = { + enable = true; + path = "ou=users"; + objectClass = "inetOrgPerson"; + emailAttribute = "skMail"; + filter = "(|(memberOf=cn=skynet-committee,ou=groups,dc=skynet,dc=ie)(memberOf=cn=skynet-admins,ou=groups,dc=skynet,dc=ie))"; + }; + groups = { + enable = true; + path = "ou=groups"; + objectClass = "groupOfNames"; + nameAttribute = "cn"; + filter = ""; + }; + }; + + env = { + bitwarden = config.age.secrets.bitwarden_sync_api.path; + ldap = config.age.secrets.bitwarden_sync_ldap.path; + }; + }; + }; +} diff --git a/secrets/bitwarden/api.age b/secrets/bitwarden/api.age new file mode 100644 index 0000000..e92f6e7 --- /dev/null +++ b/secrets/bitwarden/api.age @@ -0,0 +1,17 @@ +age-encryption.org/v1 +-> ssh-ed25519 V1pwNA 9sIoEpzKd/eI94AuhnxT1jyTIpLiqvNLvZ2oDqEzXUY +YstVwGRjZUXguF+MVJrzi4pj4h3YJI222mw0yzZf6NQ +-> ssh-ed25519 4PzZog 7kF/5y4OqdF88N4Dhx7G93fUCO2RwR+6QxWn5tH6RVQ +cV2hwmEhwGWIjpktlUnXDvBU8Zlc0nHNfDgrhNnH9+g +-> ssh-ed25519 5Nd93w Wjt9rcp1YEgkt9/P8vYUeVbNA420drbz/mZZERZFUGU +VE5a0Wx5WTy12cCm2Vg3J8GYQ1B+WnEca/FTFPhZ3nE +-> ssh-ed25519 q8eJgg EmdkKgMt9LkZSVm0pN0vf35p8UwpBWzF/cC32VviyQM +Ii+g+vgMoCj9XYpCoOyTD4sahYNUhbQRoDwgDnZCUEU +-> ssh-ed25519 IzAMqA pNlr1079F7f8zqfb4bujzQPNahoKUBH4GShDu9g2r30 +FUa1QqHBLy2qb4eHYeZgQetyjX44LnckPlv46694Sds +-> 1-grease Jr S68AA 6z@gP Y) + +--- mEkHKhEzkas0RT9tzEVFeEenFW6Av4E0uXzCeYgCdRA +,BU@!*y +C1!t{b*leHOh#/V,5J:)4f