commit 3980c37fa05bc37133f4e39637529145fae32d89 Author: Peter Jones Date: Wed Dec 19 16:08:03 2018 -0700 security, rails, postgresql: Import files from original repo diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6bf9a5c --- /dev/null +++ b/LICENSE @@ -0,0 +1,26 @@ +Copyright (c) 2016-2018 Peter J. Jones +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the + distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..4e54ec1 --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +Phoebe is a set of [NixOS][] modules that provide additional +functionality on top of the existing modules in [Nixpkgs][]. The name +of this package was taken from the name of [Saturn's moon][phoebe]. + +Module List +----------- + + * `phoebe.security`: + + Automatically enable various security related settings for NixOS. + + * `phoebe.services.postgresql`: + + Start and manage PostgreSQL, including automatic user and database + creation. + + * `phoebe.services.rails`: + + Configure and manage Ruby on Rails applications. Includes a + helper function to help package Rails applications so they can be + used by this service. + + +[nixos]: https://nixos.org/ +[nixpkgs]: https://nixos.org/nixpkgs/ +[phoebe]: https://en.wikipedia.org/wiki/Phoebe_(moon) diff --git a/default.nix b/default.nix new file mode 100644 index 0000000..2739a28 --- /dev/null +++ b/default.nix @@ -0,0 +1,7 @@ +{ config, lib, pkgs, ...}: + +{ + imports = [ + ./modules + ]; +} diff --git a/helpers.nix b/helpers.nix new file mode 100644 index 0000000..9e6c95f --- /dev/null +++ b/helpers.nix @@ -0,0 +1,14 @@ +{ pkgs }: + +let + callPackage = pkgs.lib.callPackageWith self; + + self = { + inherit pkgs; + + rails = callPackage ./modules/services/web/rails/helpers.nix { }; + }; +in +{ + inherit (self.rails) mkRailsDerivation; +} diff --git a/modules/default.nix b/modules/default.nix new file mode 100644 index 0000000..e44fc93 --- /dev/null +++ b/modules/default.nix @@ -0,0 +1,8 @@ +{ config, lib, pkgs, ...}: + +{ + imports = [ + ./security + ./services + ]; +} diff --git a/modules/security/default.nix b/modules/security/default.nix new file mode 100644 index 0000000..b83813f --- /dev/null +++ b/modules/security/default.nix @@ -0,0 +1,41 @@ +{ config, lib, pkgs, ...}: + +# Bring in library functions: +with lib; + +let + cfg = config.phoebe.security; + +in +{ + #### Interface + options.phoebe.security = { + enable = mkOption { + type = types.bool; + default = true; + description = '' + Whether or not to enable security settings. Usually this will + be left at the default value of true. However, for testing + inside virtual machines you probably wnat to turn this off. + ''; + }; + }; + + #### Implementation + config = mkMerge [ + (mkIf (!cfg.enable) { + # Only really useful for development VMs: + networking.firewall.enable = false; + }) + + (mkIf cfg.enable { + # Firewall: + networking.firewall = { + enable = true; + allowPing = true; + pingLimit = "--limit 1/minute --limit-burst 5"; + allowedTCPPorts = config.services.openssh.ports; + }; + }) + ]; +} diff --git a/modules/services/databases/default.nix b/modules/services/databases/default.nix new file mode 100644 index 0000000..6b0ee88 --- /dev/null +++ b/modules/services/databases/default.nix @@ -0,0 +1,7 @@ +{ config, lib, pkgs, ...}: + +{ + imports = [ + ./postgresql + ]; +} diff --git a/modules/services/databases/postgresql/create-user.nix b/modules/services/databases/postgresql/create-user.nix new file mode 100644 index 0000000..2e461a4 --- /dev/null +++ b/modules/services/databases/postgresql/create-user.nix @@ -0,0 +1,24 @@ +{ config, lib, pkgs, ...}: + +pkgs.stdenvNoCC.mkDerivation { + name = "pg-create-user"; + phases = [ "installPhase" "fixupPhase" ]; + + installPhase = '' + # Substitution variables: + export sudo=${pkgs.sudo}/bin/sudo + export superuser=${config.services.postgresql.superUser} + + mkdir -p $out/bin $out/sql + cp ${./create-user.sql} $out/sql/create-user.sql + substituteAll ${./create-user.sh} $out/bin/create-user.sh + chmod 555 $out/bin/create-user.sh + ''; + + meta = with lib; { + description = "Automatically create PosgreSQL databases and users as needed."; + homepage = https://git.devalot.com/pjones/phoebe/; + maintainers = with maintainers; [ pjones ]; + platforms = platforms.all; + }; +} diff --git a/modules/services/databases/postgresql/create-user.sh b/modules/services/databases/postgresql/create-user.sh new file mode 100755 index 0000000..4b51c48 --- /dev/null +++ b/modules/services/databases/postgresql/create-user.sh @@ -0,0 +1,120 @@ +#!/bin/bash + +################################################################################ +set -e + +################################################################################ +option_username="" +option_password_file="" +option_database="" +option_extensions="" +option_sqlfile="@out@/sql/create-user.sql" + +################################################################################ +usage () { +cat <&2 echo "ERROR: password for $option_username contains single quote!" + exit 1 + fi + + password=$(head -n 1 "$option_password_file") + + awk -v 'USERNAME'="$option_username" \ + -v 'PASSWORD'="$password" \ + ' { gsub(/@@USERNAME@@/, USERNAME); + gsub(/@@PASSWORD@@/, PASSWORD); + print; + } + ' < "$option_sqlfile" > "$tmp_sql_file" + + # Let the database user read the generated file. + chmod go+r "$tmp_sql_file" +} + +################################################################################ +create_user() { + mksql + _psql -d postgres -f "$tmp_sql_file" > /dev/null +} + +################################################################################ +create_database() { + has_db=$(_psql -tAl | cut -d'|' -f1 | grep -cF "$option_database" || :) + + if [ "$has_db" -eq 0 ]; then + @sudo@ -u @superuser@ -H \ + createdb --owner "$option_username" "$option_database" + fi +} + +################################################################################ +enable_extensions() { + if [ -n "$option_extensions" ]; then + for ext in $option_extensions; do + _psql "$option_database" -c "CREATE EXTENSION IF NOT EXISTS $ext" + done + fi +} + +################################################################################ +create_user +create_database +enable_extensions diff --git a/modules/services/databases/postgresql/create-user.sql b/modules/services/databases/postgresql/create-user.sql new file mode 100644 index 0000000..09f00e5 --- /dev/null +++ b/modules/services/databases/postgresql/create-user.sql @@ -0,0 +1,12 @@ +DO +$body$ +BEGIN + IF NOT EXISTS ( + SELECT + FROM pg_catalog.pg_roles + WHERE rolname = '@@USERNAME@@') THEN + + CREATE ROLE @@USERNAME@@ LOGIN ENCRYPTED PASSWORD '@@PASSWORD@@'; + END IF; +END +$body$; diff --git a/modules/services/databases/postgresql/default.nix b/modules/services/databases/postgresql/default.nix new file mode 100644 index 0000000..b1140b8 --- /dev/null +++ b/modules/services/databases/postgresql/default.nix @@ -0,0 +1,145 @@ +# Configure PostgreSQL: +{ config, lib, pkgs, ...}: + +# Bring in library functions: +with lib; + +let + cfg = config.phoebe.services.postgresql; + superuser = config.services.postgresql.superUser; + create-user = import ./create-user.nix { inherit config lib pkgs; }; + afterservices = concatMap (a: a.afterServices) (attrValues cfg.accounts); + + # Per-account options: + account = { name, ... }: { + + #### Interface: + options = { + user = mkOption { + type = types.str; + default = null; + example = "jdoe"; + description = "The name of the account (username)."; + }; + + passwordFile = mkOption { + type = types.path; + default = null; + example = "/run/keys/pgpass.txt"; + description = '' + A file containing the password of this database user. + You'll want to use something like NixOps to get the password + file onto the target machine. + ''; + }; + + afterServices = mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ "dbpassword.service" ]; + description = '' + A list of services that need to run before this user account + can be created. This is really useful if you are using + NixOps to deploy the password file and want to wait for the + key to appear in /run/keys. + ''; + }; + + database = mkOption { + type = types.str; + default = null; + example = "jdoe"; + description = '' + The name of the database this user can access. Defaults to + the account name. + ''; + }; + + extensions = mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ "pg_trgm" ]; + description = "A list of extension modules to enable for the database."; + }; + + netmask = mkOption { + type = types.nullOr types.str; + default = null; + example = "127.0.0.1/32"; + description = '' + IP netmask of remote machines allowed to connect. Leaving + this at it's default value means this account can only + connect through Unix domain sockets. + ''; + }; + }; + + #### Implementation: + config = { + user = mkDefault name; + database = mkDefault name; + }; + }; + + # Create HBA authentication entries: + accountToHBA = account: + '' + local ${account.database} ${account.user} md5 + host ${account.database} ${account.user} 127.0.0.1/32 md5 + host ${account.database} ${account.user} ::1/28 md5 + '' + optionalString (account.netmask != null) '' + host ${account.database} ${account.user} ${account.netmask} md5 + ''; + + # Commands to run to create accounts/databases: + createScript = account: + '' + ${create-user}/bin/create-user.sh \ + -u "${account.user}" \ + -d "${account.database}" \ + -p "${account.passwordFile}" \ + -e "${concatStringsSep " " account.extensions}" + ''; + +in +{ + #### Interface + options.phoebe.services.postgresql = { + enable = mkEnableOption "PostgreSQL"; + + accounts = mkOption { + type = types.attrsOf (types.submodule account); + default = { }; + description = "Additional user accounts"; + }; + }; + + #### Implementation + config = mkIf cfg.enable { + + # Set up PosgreSQL: + services.postgresql = { + enable = true; + enableTCPIP = true; + package = pkgs.postgresql; + + # The superuser can access all databases locally, remote access + # for some users. + authentication = mkForce ( + "local all ${superuser} peer\n" + + "host all ${superuser} 127.0.0.1/32 ident\n" + + "host all ${superuser} ::1/128 ident\n" + + concatMapStringsSep "\n" accountToHBA (attrValues cfg.accounts)); + }; + + # Create missing accounts: + systemd.services.pg-accounts = mkIf (length (attrValues cfg.accounts) > 0) { + description = "PostgreSQL Account Manager"; + path = [ pkgs.gawk config.services.postgresql.package ]; + script = (concatMapStringsSep "\n" createScript (attrValues cfg.accounts)); + wantedBy = [ "postgresql.service" ]; + after = [ "postgresql.service" ] ++ afterservices; + wants = afterservices; + }; + }; +} diff --git a/modules/services/default.nix b/modules/services/default.nix new file mode 100644 index 0000000..d0e8311 --- /dev/null +++ b/modules/services/default.nix @@ -0,0 +1,8 @@ +{ config, lib, pkgs, ...}: + +{ + imports = [ + ./databases + ./web + ]; +} diff --git a/modules/services/web/default.nix b/modules/services/web/default.nix new file mode 100644 index 0000000..4dee0b7 --- /dev/null +++ b/modules/services/web/default.nix @@ -0,0 +1,7 @@ +{ config, lib, pkgs, ...}: + +{ + imports = [ + ./rails + ]; +} diff --git a/modules/services/web/rails/database.yml b/modules/services/web/rails/database.yml new file mode 100644 index 0000000..86ad9b7 --- /dev/null +++ b/modules/services/web/rails/database.yml @@ -0,0 +1,10 @@ +<%= ENV['RAILS_ENV'] %>: + adapter: postgresql + encoding: unicode + pool: 5 + timeout: 5000 + host: <%= ENV['DATABASE_HOST'] %> + port: <%= ENV['DATABASE_PORT'] %> + database: <%= ENV['DATABASE_NAME'] %> + username: <%= ENV['DATABASE_USER'] %> + password: '<%= File.read(ENV['DATABASE_PASSWORD_FILE']).chomp %>' diff --git a/modules/services/web/rails/db-migrate.sh b/modules/services/web/rails/db-migrate.sh new file mode 100755 index 0000000..14d4bca --- /dev/null +++ b/modules/services/web/rails/db-migrate.sh @@ -0,0 +1,58 @@ +#!/bin/bash + +################################################################################ +# Migrate a Ruby on Rails database to its latest version (which might +# mean going back in time for a rollback). +set -e +set -u + +################################################################################ +option_env=${RAILS_ENV:-production} +option_root=$(pwd) + +################################################################################ +usage () { +cat <