initial commit

master
Michele Guerini Rocco 2020-10-20 01:11:28 +02:00
commit a8947d6822
Signed by: rnhmjoj
GPG Key ID: BFBAF4C975F76450
33 changed files with 2822 additions and 0 deletions

3
.gitattributes vendored Normal file
View File

@ -0,0 +1,3 @@
#pattern filter=crypt diff=crypt
secrets/*/** filter=crypt diff=crypt
secrets/default.nix filter=crypt diff=crypt

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
result

71
assets/magnetico-merge.py Normal file
View File

@ -0,0 +1,71 @@
#!/usr/bin/env python3
import sqlite3
import argparse
def main(main_db, merged_db):
print(f"Merging {merged_db} into {main_db}")
connection = sqlite3.connect(main_db)
connection.row_factory = sqlite3.Row
connection.text_factory = bytes
cursor = connection.cursor()
cursor.execute("ATTACH ? AS merged_db", (merged_db,))
print("Gathering database statistics:")
cursor.execute("SELECT count(*) from merged_db.torrents")
total_merged = cursor.fetchone()[0]
cursor.execute(
"SELECT name FROM pragma_table_info('files') "
"WHERE name not in ('id', 'torrent_id')"
)
remaining_file_colums = [row[0].decode() for row in cursor]
cursor.execute(
"SELECT name FROM pragma_table_info('torrents')"
"WHERE name not in ('id')"
)
remaining_torrent_colums = [row[0].decode() for row in cursor]
print(f"{total_merged} torrents to merge.")
insert_files_statement = (
f"INSERT INTO files (torrent_id, {','.join(remaining_file_colums)}) "
f"SELECT ?, {','.join(remaining_file_colums)} "
f"FROM merged_db.files WHERE torrent_id = ?"
)
insert_torrents_statement = (
f"INSERT INTO torrents ({','.join(remaining_torrent_colums)})"
f"VALUES ({','.join('?' * len(remaining_torrent_colums))})"
)
failed_count = 0
cursor.execute("BEGIN")
merged = cursor.execute("SELECT * FROM merged_db.torrents")
for i, row in enumerate(merged):
try:
torrent_merge = connection.execute(
insert_torrents_statement, (*row[1:],))
# Now merge files
connection.execute(
insert_files_statement, (torrent_merge.lastrowid, row["id"]))
except sqlite3.IntegrityError:
failed_count += 1
print("Comitting… ", end="")
connection.commit()
print("OK."
f"{total_merged} torrents processed.",
f"{total_merged - failed_count} new torrents added.",
sep="\n")
connection.close()
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Tool to merge magnetico DBs")
parser.add_argument("main", type=str,
help="main dabatase")
parser.add_argument("merge", type=str,
help="dabatase to merge into main")
args = parser.parse_args()
main(args.main, args.merge)

439
configuration.nix Normal file
View File

@ -0,0 +1,439 @@
{ config, lib, pkgs, ... }:
{
imports = [
./hardware.nix
./variables.nix
./packages.nix
./jobs.nix
./matrix.nix
./magnetico.nix
./nameserver.nix
./custom
./secrets
];
### State
# Stateful things to do before updating:
# 1. Postgres migration
# 2. Matrix Synapse migration
system.stateVersion = "20.03";
boot.kernelPackages = pkgs.linuxPackages_latest;
boot.tmpOnTmpfs = true;
boot.kernel.sysctl = {
# avoid OOM hangs
"vm.admin_reserve_kbytes" = 262144;
};
time.timeZone = "Europe/Rome";
i18n.defaultLocale = "en_US.UTF-8";
systemd.enableEmergencyMode = false;
networking = {
hostName = "maxwell";
firewall.allowedTCPPorts = [
443 80 # reverse proxy
8080 # hubot
5349 # turn server
5350 # turn server
3551 # apcups
5001 # iperf server
18080 # monero p2p
20000 # syncthing transfert
64738 # mumble server
];
firewall.allowedUDPPorts = [
53 # powerdns
1194 # dnscrypt
21027 # syncthing discovery
64738 # mumble server
];
firewall.allowedUDPPortRanges = [
{ from=49152; to=49999; } # turn relay
];
usePredictableInterfaceNames = false;
nameservers = [ "127.0.0.1" ];
hosts."127.0.0.1" = [ config.var.hostname ];
};
# Only declarative users and no password logins
users.mutableUsers = false;
users.users ={
# Only needed for local (read emergency) shell access
root.passwordFile = config.secrets.passwords.root;
# Admin
rnhmjoj = {
uid = 1000;
extraGroups = [ "wheel" ];
isNormalUser = true;
shell = pkgs.fish;
openssh.authorizedKeys.keyFiles = [ config.secrets.publicKeys.rnhmjoj ];
};
# Admin
fazo = {
extraGroups = [ "wheel" ];
isNormalUser = true;
openssh.authorizedKeys.keyFiles = [ config.secrets.publicKeys.fazo];
};
# Runs two chatbots
meme = {
extraGroups = [ "ubino" "miguelbridge" ];
isNormalUser = true;
shell = pkgs.fish;
openssh.authorizedKeys.keyFiles = [ config.secrets.publicKeys.meme ];
};
# Hosts the cactalogo
giu = {
isNormalUser = true;
shell = pkgs.fish;
openssh.authorizedKeys.keyFiles = with config.secrets.publicKeys;
[ rnhmjoj giu ];
};
# Needed to perform remote builds on Maxwell
builder = {
description = "Remote Nix builds user";
isNormalUser = true;
openssh.authorizedKeys.keyFiles = [ config.secrets.publicKeys.rnhmjoj-builder ];
};
# Use "git" instead of the default name to make
# SSH operation handier, example:
# git clone git@maxwell:user/repo
git = {
description = "Git server user";
home = "/var/lib/gitea";
useDefaultShell = true;
};
};
# Generate Diffie-Hellman parameters
# for TLS applications, like nginx.
security.dhparams = {
enable = true;
params.nginx = 2048; # prime modulus bits
};
security.sudo = {
enable = true;
# Users don't have a password
wheelNeedsPassword = false;
extraConfig =
let
path = "/run/current-system/sw/bin";
journal = name: "${path}/journalctl -* ${name}";
services = lib.concatMapStringsSep "," (name: "${journal name}");
in ''
# Allow meme to see his logs.
Cmnd_Alias MEME_UNITS = ${services ["ubino" "miguelbridge"]}
meme ALL=(root) NOPASSWD: MEME_UNITS
'';
};
security.polkit.extraConfig = ''
// Allow meme to manage his services.
polkit.addRule(function(action, subject) {
if (action.id == "org.freedesktop.systemd1.manage-units" &&
subject.user == "meme" &&
(action.lookup("unit") == "ubino.service" ||
action.lookup("unit") == "miguelbridge.service")) {
return polkit.Result.YES;
}
});
'';
# Limit user process to stop fork bombs
security.pam.loginLimits = [
{ domain = "@users";
type = "hard";
item = "nproc";
value = "400";
}
];
### ACME certificates
security.acme = with config.var; {
email = "rnhmjoj@inventati.org";
acceptTerms = true;
certs."${hostname}" = {
group = "maxwell-ydns-eu";
};
certs."riot.${hostname}" = {
group = "riot-maxwell-ydns-eu";
};
};
# Allow read access to ACME certificate
# to specific (service) users.
users.groups."maxwell-ydns-eu".members = [ "murmur" "turnserver" ];
users.groups."riot-maxwell-ydns-eu".members = [ "nginx" ];
services.openssh = {
enable = true;
permitRootLogin = "no";
passwordAuthentication = false;
challengeResponseAuthentication = false;
};
# Traceroute easter egg
services.fakeroute = {
enable = true;
route = [
"89.111.117.32" "99.97.110.110" "111.116.32.104" "105.100.101.46"
"32.73.32.115" "101.101.32.121" "111.117.46.32" "84.104.101.114"
"101.32.105.115" "32.110.111.32" "108.105.102.101" "32.105.110.32"
"116.104.101.32" "118.111.105.100" "46.32.79.110" "108.121.32.100"
"101.97.116.104" ];
};
### Mumble server
services.murmur = {
enable = true;
password = "allwellthatmaxwell";
registerHostname = config.var.hostname;
registerName = "Maxwell Mumble";
registerPassword = config.secrets.murmur.password;
users = 10;
extraConfig = with config.var; ''
sslCert=/var/lib/acme/${hostname}/fullchain.pem
sslKey=/var/lib/acme/${hostname}/key.pem
'';
};
### Syncthing node
services.syncthing = {
enable = true;
openDefaultPorts = true;
};
### Monero node with local RPC
services.monero = {
enable = true;
mining = {
enable = false;
threads = 4;
address = config.secrets.monero.address;
};
limits = {
upload = 250;
download = 625;
threads = 4;
};
rpc.user = config.secrets.monero.user;
rpc.password = config.secrets.monero.password;
};
### URL shortner
services.breve = {
enable = true;
hostname = "localhost";
baseUrl = "https://brve.bit/";
port = 2000;
certificate = "/var/lib/breve/breve.crt";
key = "/var/lib/breve/breve.key";
};
### Git server
services.gitea = with config.var; {
enable = true;
domain = hostname;
appName = "Maxwell git server";
rootUrl = "https://${hostname}/git/";
user = "git";
database.user = "git";
log.level = "Error";
cookieSecure = true;
disableRegistration = false;
settings = {
security.LOGIN_REMEMBER_DAYS = 365;
attachment.MAX_SIZE = 10;
};
};
### Searx instance
services.searx = {
enable = true;
configFile = ./assets/searx-settings.yml;
};
### Reverse Proxy
services.nginx =
with config.var;
let
disableLog = ''
error_log syslog:server=unix:/dev/log crit;
access_log off;
'';
enableSTS = ''
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
'';
in
{
enable = true;
enableReload = true;
commonHttpConfig = ''
# recommendedTlsSettings = true;
# android doesn't like this one:
# ssl_ecdh_curve secp384r1;
ssl_session_cache shared:SSL:42m;
ssl_session_timeout 23m;
ssl_prefer_server_ciphers on;
ssl_stapling on;
ssl_stapling_verify on;
'';
recommendedGzipSettings = true;
recommendedProxySettings = true;
# Large enough to allow file uploads.
clientMaxBodySize = "1000M";
sslDhparam = "${config.security.dhparams.path}/nginx.pem";
# Maxwell
virtualHosts."${hostname}" =
{
enableACME = true;
forceSSL = true;
default = true;
extraConfig = disableLog + enableSTS;
# Returns IP address
locations."/ip".extraConfig = "return 200 $remote_addr;";
# Asjon code coverage reports
locations."/asjon/report/" = {
index = "index.html";
alias = "/var/lib/asjon/tree/report/";
};
# Searx instance
locations."/srx/" = {
proxyPass = "http://localhost:8083/";
extraConfig = ''
proxy_set_header X-Scheme $scheme;
proxy_set_header X-Script-Name /srx/;
proxy_buffering off;
'';
};
# Git server
locations."/git/" .proxyPass = "http://localhost:3000/";
# Syncthing
locations."/sync/".proxyPass = "http://localhost:8384/";
};
# Breve URL shortner
virtualHosts."brve.bit" = {
forceSSL = true;
sslCertificate = "/var/lib/breve/breve.crt";
sslCertificateKey = "/var/lib/breve/breve.key";
locations."/" = {
proxyPass = "https://localhost:2000";
extraConfig = "proxy_ssl_verify off;";
};
extraConfig = disableLog;
};
# The Cactalogue
virtualHosts."cacta.bit" = {
locations."/".alias = "/home/giu/cactalogue/";
extraConfig = disableLog;
};
};
### Misc. services
services.ubino.enable = true;
services.miguelbridge.enable = true;
services.asjon.enable = true;
# Needed for the Asjon memory module
services.redis.enable = true;
### Program configuration
programs = {
fish.enable = true;
mosh.enable = true;
tmux = {
enable = true;
newSession = true;
baseIndex = 1;
escapeTime = 0;
historyLimit = 4096;
keyMode = "vi";
terminal = "screen-256color";
customPaneNavigationAndResize = true;
extraConfig = ''
set -g mouse on
# bindings
bind | split-window -h
bind - split-window -v
bind : command-prompt
bind -n C-k clear-history
# colors
set -g pane-border-style fg=brightblack
set -g pane-active-border fg=green
set -g message-style fg=white,bg=black
set -g status-style fg=brightblue,bg=black
setw -g mode-style fg=black,bg=cyan
# status line
set -g status on
set -g status-justify left
set -g status-left ""
set -g status-right-length 60
set -g status-right '#[fg=yellow]#(cut -d\ -f 1-3 /proc/loadavg) | #[fg=brightgreen]%a %H:%M'
setw -g window-status-format "#[fg=black#,bg=brightblack] #I #[fg=blue#,bg=black] #W "
setw -g window-status-current-format "#[fg=white#,bg=cyan] #I #[fg=black#,bg=brightblack] #W "
'';
};
};
nix = {
useSandbox = true;
# Can connect to the Nix daemon
# and upload/run code as root!
trustedUsers = [ "builder" "rnhmjoj" ];
# Use at most half the cores
buildCores = 8;
extraOptions = ''
# Always keep at least 256MiB free
min-free = 268435456
'';
};
environment.variables = {
PATH = "$HOME/.local/bin/:$PATH";
XDG_CONFIG_HOME = "$HOME/.config";
XDG_DATA_HOME = "$HOME/.local/share";
XDG_CACHE_HOME = "$HOME/.cache";
NIX_PROFILE = "$XDG_CONFIG_HOME/nix/profile";
};
# Needed to make the mosh server survive a
# user logout: systemd kills everything by default
environment.shellAliases = {
mosh-server = "systemd-run --user --scope mosh-server";
};
}

17
custom/default.nix Normal file
View File

@ -0,0 +1,17 @@
{ ... }:
# These are custom NixOS modules that are
# not yet in Nixpkgs or can't be upstreamed.
{
imports =
[ # Misc. system services
./modules/breve.nix
./modules/asjon.nix
./modules/ubino.nix
./modules/miguelbridge.nix
# Safely handle secrets
./modules/secrets-store.nix
];
}

109
custom/modules/asjon.nix Normal file
View File

@ -0,0 +1,109 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.asjon;
in {
options.services.asjon = {
enable = mkEnableOption "Asjon: our chat bot";
dataDir = mkOption {
type = types.path;
default = "/var/lib/asjon";
description = ''
Path where the settings and source tree will exist.
'';
};
user = mkOption {
type = types.str;
default = "asjon";
description = ''
Asjon will be run under this user (user will be created if it doesn't exist.
This can be your user name).
'';
};
};
config = mkIf cfg.enable {
users.extraUsers."${cfg.user}" = {
home = cfg.dataDir;
createHome = true;
description = "asjon user";
shell = "${pkgs.bash}/bin/bash";
};
systemd.services.asjon = {
description = "asjon: our chat bot";
after = [ "nginx.service" "matrix-synapse.service" "asjon-init.service" ];
requires = [ "nginx.service" "matrix-synapse.service" "asjon-init.service" ];
wantedBy = [ "multi-user.target" ];
path = with pkgs; [
nodejs nodePackages.coffee-script
yarn openssh graphicsmagick git
bash
];
environment = {
# Matrix login
HUBOT_MATRIX_HOST_SERVER = "https://${config.var.hostname}";
# Git integration
HUBOT_GIT_URL = "https://${config.var.hostname}/git";
HUBOT_GIT_API = "https://${config.var.hostname}/git/api/v1";
HUBOT_GIT_REPO = "rnhmjoj/asjon";
# Scripts
AUTO_KILL_ON_UPDATE = "1";
AUTO_INFORM_ON_START = "!kvLvoCovzInhiablSq:maxwell.ydns.eu";
ADMIN_ROOM = "!kvLvoCovzInhiablSq:maxwell.ydns.eu";
REV_REMOTE_HOST = "proxy@rnhmjoj.ydns.eu";
REV_REMOTE_PORT = "22";
REV_KEY = "~/.ssh/proxy";
};
serviceConfig = {
User = cfg.user;
ExecStart = "${cfg.dataDir}/tree/bin/hubot -a matrix";
Restart = "always";
WorkingDirectory = "${cfg.dataDir}/tree";
# API keys and passwords definitions
EnvironmentFile = config.secrets.asjon.environment;
};
};
systemd.services.asjon-init = {
description = "Initialize Asjon service (first time only)";
wants = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig.User = cfg.user;
path = with pkgs; [ git yarn acl ];
script = ''
if test -d ${cfg.dataDir}/tree/.git; then
exit 0
fi
# clone repository and install packages
git clone https://github.com/rnhmjoj/asjon.git ${cfg.dataDir}/tree
cd ${cfg.dataDir}/tree
yarn install
# give read/traverse permission to nginx
setfacl -m g:nginx:x ${cfg.dataDir}
setfacl -m g:nginx:x ${cfg.dataDir}/tree
setfacl -Rdm g:nginx:rx ${cfg.dataDir}/tree/report
'';
};
};
}

137
custom/modules/breve.nix Normal file
View File

@ -0,0 +1,137 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.breve;
dataDir = "/var/lib/breve";
configFile = pkgs.writeText "breve.conf" ''
hostname = "${cfg.hostname}"
port = ${toString cfg.port}
baseurl = "${cfg.baseUrl}"
urltable = "${dataDir}/urls"
tls {
cert = "${cfg.certificate}"
key = "${cfg.key}"
}
'';
in {
options.services.breve = {
enable = mkEnableOption ''
Breve: a url shortner service.
'';
openPorts = mkOption {
type = types.bool;
default = false;
example = literalExample "true";
description = ''
Open the default ports in the firewall:
- TCP 443 (or specific port) for HTTPS
- TCP 80 (or specific port) for HTTP->HTTPS redirect
'';
};
user = mkOption {
type = types.str;
default = "breve";
description = ''
Breve will run under this user (user will be created if it doesn't exist.
This can be your user name).
'';
};
hostname = mkOption {
type = types.str;
default = config.networking.hostName;
description = ''
Breve will bind and generate URLs accorting to this hostname.
'';
};
baseUrl = mkOption {
type = types.str;
default = "https://localhost:3000/";
example = "https://example.com";
description = ''
URL to reach the breve index page. Needed in case Breve is served by
a reverse proxy on a different url.
'';
};
port = mkOption {
type = types.int;
default = 443;
example = 8080;
description = ''
Breve main interface will be listening on this port.
'';
};
certificate = mkOption {
type = types.path;
default = "${dataDir}/breve.crt";
description = ''
The TLS certificate that Breve will be using to encrypt traffic.
'';
};
key = mkOption {
type = types.path;
default = "${dataDir}/breve.key";
description = ''
The TLS key that Breve will be using to encrypt traffic.
'';
};
certificateChain = mkOption {
type = types.listOf types.path;
default = [];
description = ''
List of paths to the TLS certificates chain.
'';
};
};
config = mkIf cfg.enable {
users.extraUsers."${cfg.user}" = {
isSystemUser = true;
description = "Breve daemon user";
};
networking.firewall = mkIf cfg.openPorts {
allowedTCPPorts = [ cfg.port ]
++ optional (cfg.port == 443) 80;
};
systemd.services.breve = {
description = "breve: url shortner";
wants = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
environment.XDG_CONFIG_HOME = "${dataDir}/conf";
serviceConfig = {
User = cfg.user;
ExecStart = "${pkgs.haskellPackages.breve}/bin/breve";
Restart = "on-failure";
StateDirectory = "breve";
};
preStart = ''
# link configuration
mkdir -p ${dataDir}/conf
if [ "$(realpath ${dataDir}/conf/breve)" != "${configFile}" ]; then
rm -f ${dataDir}/conf/breve
ln -s ${configFile} ${dataDir}/conf/breve
fi
'';
};
};
}

View File

@ -0,0 +1,52 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.miguelbridge;
in {
options.services.miguelbridge = {
enable = mkEnableOption "miguelbridge: Bridge Telegram - Matrix.";
user = mkOption {
type = types.str;
default = "miguelbridge";
description = ''
miguelbridge will be run under this user (user will be created if it doesn't exist.
This can be your user name).
'';
};
};
config = mkIf cfg.enable {
users.groups.miguelbridge = {};
users.extraUsers."${cfg.user}" = {
isSystemUser = true;
group = "miguelbridge";
description = "miguelbridge user";
};
systemd.services.miguelbridge = {
description = "miguelbridge: Bridge Telegram - Matrix";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
User = cfg.user;
Group = "miguelbridge";
ExecStart = "${pkgs.openjdk}/bin/java -jar MiguelBridge.jar";
Restart = "always";
StateDirectory = "miguelbridge";
WorkingDirectory = "%S/miguelbridge";
};
};
};
}

View File

@ -0,0 +1,117 @@
{ config, pkgs, lib, ... }:
with lib;
let
cfg = config.security.runtimeSecrets;
# A recursive attrset of submodule
storeType = types.attrsOf (types.submodule
{ freeformType = storeType;
options = secretOptions;
});
# Secret file definition
secretOptions =
{ user = mkOption
{ type = types.str;
default = "root";
description = "Owner of the secret";
};
group = mkOption
{ type = types.str;
default = "root";
description = "Group with access to the secret.";
};
mode = mkOption
{ type = types.str;
default = "0400";
description = "File permission (octal format)";
};
path = mkOption
{ type = types.nullOr types.path;
default = null;
apply = toString;
description = "File to include in the secret store";
};
};
# Turns a nested attrset into a list
# of (path, value) pairs. It recurs
# until `cond val` is false.
attrsToIndex = cond: set:
let recurse = path: set:
let index = name: value:
if isAttrs value && cond value
then recurse (path ++ [name]) value
else singleton { path = path ++ [name]; value = value; };
in concatLists (mapAttrsToList index set);
in recurse [] set;
isFile = v: isAttrs v && v.path != "";
# Secrets flattened to an index. This is needed
# to iterate over the set.
flatSecrets = attrsToIndex (v: !isFile v) cfg;
# Secrets with paths rewritten to the store location
storedSecrets = mapAttrsRecursiveCond (v: !isFile v)
(names: secret:
if isFile secret
then "/run/secret/${concatStringsSep "-" names}"
else secret) cfg;
in {
options.security.runtimeSecrets = mkOption {
type = storeType;
default = { };
description = ''
Definitions of runtime secrets. This is a freeform attributes
set: it can contain arbitrarily nested sets of secrets.
Secrets are paths to be copied into the secrets store
(/run/secrets) with proper permission and owenership.
'';
};
options.security.buildSecrets = mkOption {
type = types.attrs;
default = { };
description = ''
Definitions of build secrets. This is a freeform attrset
to be merged with the secrets-store and intended to store
unsafe secrets. This will be copied into the world-readable
Nix store, only use at a last resort.
'';
};
options.secrets = mkOption {
type = types.attrs;
readOnly = true;
default = recursiveUpdate storedSecrets config.security.buildSecrets;
description = ''
The attrset used to access stored secrets from NixOS
configuration and modules.
'';
};
config.system.activationScripts.secretsStore = {
deps = [ ];
text =
''
# Initialise clean directory
rm -rf /run/secrets
'' + concatMapStrings (pair:
let
name = "${concatStringsSep "-" pair.path}";
secret = pair.value;
in
optionalString (isFile secret)
''
# Install secret ${name}
install -m ${secret.mode} \
-o ${secret.user} -g ${secret.group} \
-D ${secret.path} /run/secrets/${name}
'') flatSecrets;
};
}

52
custom/modules/ubino.nix Normal file
View File

@ -0,0 +1,52 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.ubino;
in {
options.services.ubino = {
enable = mkEnableOption "Ubino: assistente virtuale di Ube, sottoforma di bot di Telegram.";
user = mkOption {
type = types.str;
default = "ubino";
description = ''
Ubino will be run under this user (user will be created if it doesn't exist.
This can be your user name).
'';
};
};
config = mkIf cfg.enable {
users.groups.ubino = {};
users.extraUsers."${cfg.user}" = {
isSystemUser = true;
group = "ubino";
description = "Ubino user";
};
systemd.services.ubino = {
description = "Ubino: assistente virtuale di Ube, sottoforma di bot di Telegram.";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
User = cfg.user;
Group = "ubino";
ExecStart = "${pkgs.openjdk}/bin/java -jar UbinoBot.jar";
Restart = "always";
StateDirectory = "ubino";
WorkingDirectory = "%S/ubino";
};
};
};
}

View File

@ -0,0 +1,20 @@
{ writeScriptBin, fish, curl
, homeserver
, roomId
, authToken
}:
writeScriptBin "notify" ''
#!${fish}/bin/fish
set token (cat ${authToken})
if test (id -u) != 0
echo 'you must be root to send a notice'
exit 1
end
set url '${homeserver}/rooms/${roomId}/send/m.room.message?access_token='$token
set msg '{"msgtype":"m.text", "body": "'$argv[1]'"}'
${curl}/bin/curl -s -XPOST -d $msg $url
''

85
hardware.nix Normal file
View File

@ -0,0 +1,85 @@
{ config, lib, pkgs, ... }:
{
imports = [
<nixpkgs/nixos/modules/installer/scan/not-detected.nix>
];
boot.kernelModules = [ "kvm-intel" ];
boot.initrd.availableKernelModules = [
"uhci_hcd" "ehci_pci" "ata_piix"
"usbhid" "usb_storage" "sd_mod"
];
boot.loader.grub = {
enable = true;
version = 2;
device = "/dev/sda";
};
fileSystems."/" =
{ device = "/dev/main/nixos";
fsType = "ext4";
};
fileSystems."/home" =
{ device = "/dev/main/home";
fsType = "ext4";
};
fileSystems."/var/lib" =
{ device = "/dev/data/data";
fsType = "ext4";
};
nix.maxJobs = lib.mkDefault 16;
powerManagement.cpuFreqGovernor = "ondemand";
services.apcupsd = {
enable = true;
configText = ''
UPSTYPE usb
UPSCABLE usb
NETSERVER on
NISPORT 3551
MINUTES 5
'';
hooks =
let
# Send notifications on the Maxwell
# room when something bad happens.
notify = msg: ''${pkgs.maxwell-notify}/bin/notify "UPS: ${msg}"'';
in
{
changeme = notify "sostituire le batterie";
battdetach = notify "batterie disconnesse";
battattach = notify "batterie riconnesse";
commfailure = notify "connessione persa";
commok = notify "connessione ristabilita";
loadlimit = notify "livello batterie critico (5%)";
runlimit = notify "autonomia batterie critico (5min)";
doshutdown = notify "inizio sequenza di spegnimento";
powerout = notify "rete elettrica disconnessa";
mainsback = notify "rete elettrica riconnessa";
onbattery = notify "attivate batterie";
offbattery = notify "disattivate batterie";
emergency = notify "malfunzionamento batterie, possibile spegnimento!";
};
};
services.smartd =
let
# Send a notification on the Maxwell
# when a disk is starting to fail.
failHook = with pkgs; writeScript "disk-fail-hook" ''
#!/bin/sh
${pkgs.maxwell-notify}/bin/notify \
"SMART: rilevato problema al disco $SMARTD_DEVICESTRING:"
${pkgs.maxwell-notify}/bin/notify "> $SMARTD_MESSAGE"
'';
in
{
enable = true;
defaults.monitored = "-a -M exec ${failHook}";
};
}

129
jobs.nix Normal file
View File

@ -0,0 +1,129 @@
{ config, pkgs, lib, ... }:
with lib;
{
systemd.services.ydns = {
description = "update ydns address record";
after = [ "network-online.target" ];
startAt = "*:0/30";
serviceConfig.Type = "oneshot";
serviceConfig.environmentFile = config.secrets.ydns.environment;
path = with pkgs; [ curl cacert gawk iproute ];
environment = {
YDNS_HOST = config.var.hostname;
CURL_CA_BUNDLE = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt";
};
script = ''
update() {
ret=$(curl -$1 --basic --silent \
-u "$YDNS_USER:$YDNS_PASSWD" \
"https://ydns.io/api/v1/update/?host=$YDNS_HOST&ip=$2" || exit 0)
case "$ret" in
ok)
echo "updated successfully: $YDNS_HOST ($2)"
;;
badauth)
echo "updated failed: $YDNS_HOST (authentication failed)"
;;
*)
echo "update failed: $YDNS_HOST ($ret)"
;;
esac
}
update 4 "$(curl -s -4 https://ydns.io/api/v1/ip)"
update 6 "$(ip addr show mngtmpaddr | awk '/inet6/{print $2; exit}' | cut -d/ -f1)"
'';
};
systemd.services.backup = {
description = "run system backup";
after = [ "network-online.target" ];
startAt = "weekly";
serviceConfig.Type = "oneshot";
path = with pkgs; [ bup git nfs-utils ];
environment.BUP_DIR = "/mnt/backup";
script = ''
${pkgs.fish}/bin/fish << 'EOF'
set locations \
/etc/lvm \
/etc/nixos \
/var/lib \
/home
set excluded \
/var/lib/alsa \
/var/lib/systemd \
/var/lib/udisks2 \
/var/lib/udev \
/var/lib/postgresql
# mount NFS share
mkdir -p $BUP_DIR
mount.nfs -o nolock 192.168.1.3:/maxwell $BUP_DIR
# check if properly mounted
if not mountpoint -q $BUP_DIR
echo mount failed! 1>&2
exit 1
end
# init backup
if not test -e $BUP_DIR/bupindex
bup init
end
# build indices and copy
for i in $locations
eval bup index $i --exclude=(string join " --exclude=" $excluded)
bup save -n (basename $i) $i
end
# postgresql backup
set dir /var/lib/postgresql-backup
mkdir -p $dir
sudo -u postgres pg_dumpall | gzip > $dir/db.bak
bup index $dir
bup save -n postgresql $dir
rm -rf $dir
umount /mnt/backup
EOF
'';
};
systemd.services.namecoin-update =
let
userFile = with config.services.namecoind;
pkgs.writeText "namecoin.conf" ''
rpcbind=${rpc.address}
rpcport=${toString rpc.port}
rpcuser=${rpc.user}
rpcpassword=${rpc.password}
'';
in {
description = "update namecoin names";
after = [ "namecoind.service" ];
startAt = "hourly";
path = [ pkgs.namecoind ];
serviceConfig.Type = "oneshot";
serviceConfig.ExecStart = "${pkgs.haskellPackages.namecoin-update}/bin/namecoin-update ${userFile}";
};
}

68
magnetico.nix Normal file
View File

@ -0,0 +1,68 @@
{ config, pkgs, ... }:
# Setup:
# Maxwell runs the web UI (magneticow) but doesn't
# run the crawler (magneticod) because it's too
# network intesive. The latter is run by Wigfrid,
# which periodically uploads a sqlite database.
# Once received, Maxwell merges it with the local one.
{
### Reverse proxy location
services.nginx.virtualHosts."${config.var.hostname}" =
{ locations."/dht/" = {
proxyPass = "http://localhost:8082/";
# Rewrite all absolute paths, magneticow
# was not designed to work behind a proxy.
extraConfig = ''
sub_filter_once off;
sub_filter_types *;
sub_filter 'action="/' 'action="/dht/';
sub_filter 'href="/' 'href="/dht/';
sub_filter 'src="/' 'src="/dht/';
sub_filter '/api/' '/dht/api/';
sub_filter '/feed?' '/dht/feed?';
sub_filter 'split("/")[2]' 'split("/").pop()';
'';
};
};
### Magneticow
services.magnetico = {
enable = true;
web.port = 8082;
web.credentialsFile = config.secrets.passwords.magnetico;
};
# Disable the crawler: it's run by wigfrid
systemd.services.magneticod.enable = false;
# Start the database merge as soon
# as a new one is uploaded.
systemd.paths.merge-magnetico = {
pathConfig.PathExists = "/var/lib/magnetico/update.sqlite3";
wantedBy = [ "multi-user.target" ];
};
# Merge wigfrid update database with
# the current one and restart magneticow.
systemd.services.merge-magnetico = {
path = [ pkgs.python3 ];
script = ''
set -e
systemctl stop magneticow
cd /var/lib/magnetico
python3 ${./assets/magnetico-merge.py} database.sqlite3 update.sqlite3
rm update.sqlite3
systemctl start magneticow
'';
};
# SSH access to allow uploading
# the magnetico database.
users.users.magnetico = {
useDefaultShell = true;
openssh.authorizedKeys.keyFiles = [ config.secrets.publicKeys.magnetico ];
};
}

180
matrix.nix Normal file
View File

@ -0,0 +1,180 @@
{ config, lib, pkgs, ... }:
with config.var;
let
### Element (Riot) configuration
conf = with config.var; {
default_server_config."m.homeserver" =
{ base_url = "https://${hostname}";
server_name = "Maxwell";
};
default_server_config."m.identity_server" =
{ base_url = "https://matrix.org"; };
roomDirectory.servers = [ "matrix.org" hostname ];
brand = "Maxwell matrix";
defaultCountryCode = "IT";
showLabsSettings = true;
# Use a trusted Jitsi instance
jitsi.preferredDomain = "jitsi.openspeed.org";
jitsi.externalApiUrl = "https://jitsi.openspeed.org/libs/external_api.min.js";
};
in
{
### Reverse proxy locations
services.nginx.virtualHosts."${config.var.hostname}" =
let
client =
{ "m.homeserver" = { "base_url" = "https://${config.var.hostname}"; };
"m.identity_server" = { "base_url" = "https://matrix.org"; };
};
server = { "m.server" = "${config.var.hostname}:443"; };
in
{
# Needed for matrix federation
locations."/.well-known/matrix/server".extraConfig = ''
add_header Content-Type application/json;
return 200 '${builtins.toJSON server}';
'';
# Needed for automatic homeserver
# setup of matrix clients
locations."/.well-known/matrix/client".extraConfig = ''
add_header Content-Type application/json;
add_header Access-Control-Allow-Origin *;
return 200 '${builtins.toJSON client}';
'';
# Forward matrix API calls to synapse
locations."/_matrix".proxyPass = "http://localhost:8448";
};
### Element/Riot static location
services.nginx.virtualHosts."riot.${config.var.hostname}" =
{ enableACME = true;
forceSSL = true;
locations."/" =
{ index = "index.html";
alias = (pkgs.element-web.override { inherit conf; }) + "/";
};
};
### Homeserver
services.matrix-synapse = {
enable = true;
server_name = config.var.hostname;
# Tell users about our TURN server
turn_uris = [
"turn:${config.var.hostname}:5349?transport=udp"
"turn:${config.var.hostname}:5350?transport=udp"
"turn:${config.var.hostname}:5349?transport=tcp"
"turn:${config.var.hostname}:5350?transport=tcp"
];
# Bind on localhost and used a reverse proxy
listeners = [
{ bind_address = "localhost";
port = 8448;
type = "http";
tls = false;
resources = [
{ compress = true; names = [ "client" ] ; }
{ compress = false; names = [ "federation" ]; }
];
x_forwarded = true;
}
];
# Connect to Postrges
database_type = "psycopg2";
database_args = {
user = "matrix-synapse";
database = "matrix-synapse";
};
# Make logging less verbose
logConfig = ''
version: 1
formatters:
journal_fmt:
format: '%(name)s: [%(request)s] %(message)s'
filters:
context:
(): synapse.util.logcontext.LoggingContextFilter
request: ""
handlers:
journal:
class: systemd.journal.JournalHandler
formatter: journal_fmt
filters: [context]
SYSLOG_IDENTIFIER: synapse
root:
level: WARN
handlers: [journal]
disable_existing_loggers: False
'';
allow_guest_access = true;
expire_access_token = true;
event_cache_size = "2K";
max_upload_size = "1000M";
turn_user_lifetime = "1d";
# Needed to restrict access to the TURN
# server to only our matrix users.
turn_shared_secret = config.secrets.matrix.turn;
# Needed by the register_new_matrix_user script
registration_shared_secret = config.secrets.matrix.registration;
};
# Use the Postrges database
services.postgresql.enable = true;
# Handles users behind a NAT,
# needed for reliable VoIP.
services.coturn = {
enable = true;
# Only allow users vouched for
# by the Matrix server.
lt-cred-mech = true;
use-auth-secret = true;
static-auth-secret = config.secrets.matrix.turn;
# Use maxwell certificate for TLS
realm = config.var.hostname;
cert = "/var/lib/acme/${config.var.hostname}/fullchain.pem";
pkey = "/var/lib/acme/${config.var.hostname}/key.pem";
# Port range for TURN relaying
min-port = 49152;
max-port = 49999;
# Enable TLS
secure-stun = true;
no-tcp-relay = false;
extraConfig = ''
external-ip=${config.var.ipAddress}
cipher-list=HIGH
no-loopback-peers
no-multicast-peers
denied-peer-ip=10.0.0.0-10.255.255.255
denied-peer-ip=192.168.0.0-192.168.255.255
allowed-peer-ip=192.168.1.5
user-quota=12
total-quota=1200
verbose=true
'';
};
}

44
nameserver.nix Normal file
View File

@ -0,0 +1,44 @@
{ config, ... }:
# Setup:
# PDNS recursor on port 53
# DNSCrypt wrapper on port 1194
# NCDNS for Namecoin bit. zone resolution
{
# Recursive DNS resolver
services.pdns-recursor = {
enable = true;
# Configures the bit. zone
resolveNamecoin = true;
dns.allowFrom = [ "0.0.0.0/0" ];
};
# Wrap the local recursive resolver
# in DNSCrypt on the default OpenVPN port.
# This port is chosen because it's usually
# not blocked in corporate networks.
services.dnscrypt-wrapper = {
enable = true;
address = "0.0.0.0";
port = 1194;
};
# Namecoin resolver
services.ncdns = {
enable = true;
# This is currently broken, see ncdns issue:
# https://github.com/namecoin/ncdns/issues/127
dnssec.enable = false;
};
# Namecoin daemon with RPC server
services.namecoind = {
enable = true;
# This are used by the resolver (ncdns)
# to query the blockchain.
rpc.user = config.secrets.namecoin.user;
rpc.password = config.secrets.namecoin.password;
};
}

36
packages.nix Normal file
View File

@ -0,0 +1,36 @@
{ config, pkgs, lib, ... }:
let
unstable = import <nixos-unstable> { };
in
{
nixpkgs.overlays = lib.singleton
(self: super:
{ maxwell-notify = self.callPackage ./custom/packages/maxwell-notify.nix
{ homeserver = "https://${config.var.hostname}/_matrix/client/r0";
roomId = "!FsUSHSNMPMVTFFcvJo:maxwell.ydns.eu";
authToken = config.secrets.privateKeys.matrix;
};
monero = unstable.monero;
element-web = unstable.element-web;
});
environment.systemPackages = with pkgs; [
# utilities
iftop curl ranger neovim
nix-script
jq ack
# backup
bup git nfs-utils
# admin
dnsutils
matrix-synapse
maxwell-notify
smartmontools
];
}

41
secrets/certs/breve.crt Normal file
View File

@ -0,0 +1,41 @@
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

224
secrets/certs/breve.key Normal file
View File

@ -0,0 +1,224 @@
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

43
secrets/default.nix Normal file
View File

@ -0,0 +1,43 @@
U2FsdGVkX1/qaIMcLxupTIbNeecnRqYwcELezcmn7ZPhNBAgxAY+D3RQl78ZQ4v5
/ZBlqYXq+wsEEEgWG9zM+OsBLbFzm3g2w6ZpmwKu4wxOLgZx1U5QIqR9LI9yToeN
efTzgCs+g55StfrxI8Pn/sUcfyX780zkI+vBDHHwrvEu6DS4qRPgp48+KUAOWwtu
au5OufZLIrAtarK0uO2sJJ100ZOV7PWASm7qYRzQunzg8M6xDmT+mrqT2DTMZY/Y
rr/RULvzQpykha/p5zdfBfbYxOGapcCLYmAt2uKXJ3wXiOPq/3/x/2D9ZmDwPfOl
mERF8WJ8lijW/7ECaaYajecl3o3cHwflvFDo4gmNjoC4yq0t8auSYD5Ow0bed/KM
ZS1QDJf0rf6UELNeFvg9SHNUlILrDSlkY4kEbuJwyHDSw/QvRfMJ9+Y/sNVY98g9
Wr/Wpmon0zyY5Ez7H36/bvgfaOiUJk/zjIXQy0ou1fG4lrB9tujzdiuOGKDD+pJU
sf3OKk1ORueHWclR7zxFHAkOYkvPfzM7vfuH6Mu2Wkq7Ovr23df15Hx3eWsPOiCL
vdOh2vq4Qc53aGGEgDqOwVdfcqKqH4n91kCEJ+tIk8Ma2mD3FfQsaBJHXEpKl1q3
YUWcYUuXVAz56GqA+8Lem/00Sb5lFp4cR5/uRGtFu0p0ilf/Spn75Tm5C02RaUFn
Mi8qGP8z6JDh/l+EsagDwbLVBbhh7sF+xDXfZ4TGN4FSAjJ0bD7ekneQ5wNCrz0q
/923RfZRxXw7WbUyYB4fJ6N7YmrhUcnr+ycZmH103XXpzGouIcl0FZaoPJrt5AV7
i5zqUdIEACNGJCsD3zebWAqSclMp+9OAp5+EvHXuVM77/PJMjdjeuf9bYDNypZ83
7VzjsufcoO5TKE1yPdUg4oB+4WRPUwrpjPQoMd9Jvb/1x/oO/I6lqvJwkJw/i/W+
nR77f17cuiJT1/6ACUBpmtOwg2T1E/mHznhp1vOy7YUKdgbSXCmKIUpkN1QBYqtV
AsKqETOFiTJnl8X60sGa810XJn1oPTud/nfL8VrNGyYa8QpelqIGAaxPJiHRRnZ6
TR2MPJTz7htvR9YSr9VX1NzMMHOaar1As8+PtV7pq6v5o1jmtFCm+Q1UjODMVzez
NEJV9B8EZCNXNzAy3+RWDUPnls7Es0xGc3wLqKLLjly4X+WMUqkAxJicCDx9f93/
03xb1o1Gy865VK79T29UpodnmQ+fAqRlqP+p89dBthCwt0RIyYH3mlCl2fGuYwly
oid1JxnoUAg5v6rImaZjdmjG5pP5bTcGelrPvwfk23TZOvCh7PDof1PeGC5ZPaRy
MyjN/L52gVgQYm5pkUWxySg7jYyBj/RCZUR+1Lg/zfBCR+Pb1GKB6mgg8j3idhHi
IjohUasU+VAGhTTz2Sl2zQM3YUOEmGGW4n0vteadZJMdTCO5MmhiVSvPo4fmm3ol
JYUQtbIWUm84aRuW3FIAuL+lMgiMc/fbrjOzFMPYES7s+qyfx5iRCTGP9JomgEga
v9R/E6k98oM7/JYSOiddqST38vlLfByAGdkBnemqiPb9oKMarEpfGXupIbI3znEt
fqt9FTEPO7utg3Yr66vXsP2JqA4B1gHK95bjxF3Vq36man2+TmY6Pu9I1t8P2dFK
W3qwtLSDchT4OI0BMWBhxBEvbXIyj378zA1WPgt0TWY/zqeY5MofObiYbBaLxcTq
8ppBToY4+YwwizqziG045FT6g8FbFi60VZfo7DHtW41kZ3aBxnKHZhulup3eUT77
639iaFXfXCGeiDHP5l3qlvGZJndFSTvwW3sRFaNoit7Qy7Gjf1XFJPuPfZIhUHoS
krLMyIDrdbhP37Kc3HnAt1CKF5HJCsQBp1YMkDlms39+uPEBNK3rYUQgRlT41snd
ueZAlBnQ4FZ6iJJbgbnHXw3vLQddElEf4lvHNjDO3r2875r4KWUBuwIn/1TlJ6B7
wsIlxswgZcBpcqh4YZaMzi2WWHHuaZQoleGm/ozUzHNdcQ+HfNrbiWpLd9air/zl
VJw+uFl4qgCq8vtkCjvma83Pka9dsbyp4xZe8BsXShJWJqequ8WxLS0nLumNhVCq
GiLPIFsb5/C5UBTuYOdWYsoUS348p9v2oR78yI2G5/Krt0wmq4oXz9pGUtdhTyk3
APzo3kBt7toj+EVbuqECN96Lp7NXVcBcAz5eFOQsf6twt+FTplHSNM6Utv0QXQ2m
OxzVKwr4sIiCBil6MLuLtvvfErJMWvEPQniqnLi8VtytA6LnZuLdt/3ihM+XsQtn
l2FjAE3mMzbG4fq2xNRpibFXtSt+pyyQkWGkftv0uRlvmnc5374o25/EfDbCe3qh
ZYNSBeavXT9VMAQdXPtN/D4xnluTG3QwWkD/VIcoPrdqaXVAXdYcFUQ4ckYbfft7
LO/5rAfj5YT7Upg7lSB0/h1wIfr+LFQV5flgX8tDddf2AC18ui0xm/vjRX0ecY/f
7TOrsL/kXl9fmOs8zWbqIqrviE3s8gU+9H6wdiKL6IrAfmBP7oSz6f20iAizDzOh
jJB7BHoxMr/T25RbgFSyGZGIdXu2YjJwynJnQ+8QWbTI6n2BNxmVrhYC/4eGVsIm
9PMW8YdWnM7DllbTeq4C7VFnas2NnE36j17Yjt0ouxdx8sSBzW6LfPYyxXJw3T5A
p6krltwsQGFLXH+N

9
secrets/keys/fazo.pub Normal file
View File

@ -0,0 +1,9 @@
U2FsdGVkX1+Ay4K0S+xuJiyoMRj00oMkaw5sYVvu9VBR68aypK8eLTng8xoqKwzm
uD1YNhLdh515CgHyMI7/LraT2yDYIlF+pNEfftH6U2qU5IfWSoukD59RscfaAft+
/dend9Y6HyG1WdlyPyLVabruHFScx3d+oaLwEcgggnI/M9coWnHyvBXspo6E75um
gyvFntN4GmJLf1sMQIn0I7lW9djC8nupjSTstRo5HNLM/LwlhwAYRb/jbJUOkYSK
D/SDHW5p/9OrACQzAKHFB3mg4+9SufD+cju8qIAn9uFcyCJxkri6Mz+SGqdXtSgi
fEZE2r1aCUXFa8Nq+qoYbxVue3BFlzxetC7fZrx2zWnmkSgOn7LWDn6q3B3KWeT5
om/g+Ph/RE4piKzm9m2jIx+0TlkUHlpOKAf4Xzwdaivmm6HaCNc5pt1Hw0le1fTW
JE+6BkXFDJz/8ytROujTGlMaMCB/JHgK04diEAnQJmNQnYVG03PxmRHmmqXc1czQ
OnIzyUraBCpBsHSAVsN/afC8

3
secrets/keys/giu.pub Normal file
View File

@ -0,0 +1,3 @@
U2FsdGVkX19/6c3AzyWTN5p17ujhKlbDdk91iQs9z7Q0HiyA4L7BKFT/ZOk4VNqY
2Yh7r0b1F1ScFjvKH3aJ7jYGHT0i+w3LSHsufCDATEUejN/Z9JtEIXYaodOCJaYE
YuIaLuBwWdkAPUl1lhs=

View File

@ -0,0 +1,3 @@
U2FsdGVkX1/StXpT1GfebxPB+1TyCHLo5fjFZLNkkWXnCS04WnREE2xlV7OXw0Iq
llqZTflZ/z1hSz7NuUO/vrR57RRo6icf3UXnxvJ8HD6Z9q7uxI+WpIj+ME2zij6B
Jg==

7
secrets/keys/matrix.tok Normal file
View File

@ -0,0 +1,7 @@
U2FsdGVkX19J8VE7lArWiwLIURQ8NjPEUwkOAh4m1oR0yrmBCI5u/vhwSQTC+ETb
S3b80dqDKkR8QKRxIaquJHw/KRvQqKViZbu1OsHQTPQhK//mvs6vZ8G00vfucphc
6XIuiJS0u1zbzP6CKoLlkUyVOxFsmVSmxRx8460vgqK00JHSXf82mCAXcePVfHX9
uV9w34x3QkqSzmptx1orJrWa/Y/+et19ghJ/d6Utll+kg5Ldkd6vYcSA5bYFMe6L
LzAjJDLSvRpGkwP7EH2/9Kin5qDA7OUQmrXyFvmb9viCnYSD4TUaxXHYy4SPYcPY
qgFFeNDry5PAhkqLCTKgQWylCZXNbnA7JHp5fdbQCyRFD2sNxVN9ptuqNJd5x+hf
0PzTFokhgtE=

3
secrets/keys/meme.pub Normal file
View File

@ -0,0 +1,3 @@
U2FsdGVkX18yVvW7ZvcS0Xc/LsBJmDBTjmHeODQqsVSq8AlzjHH0Z15cY2ibL0+2
/fq+Sb12nfYhXkdFePGNJl+pwTVN2KmQhQtTPUawwa0bmvqC3wPXmHn8O1AndVP9
8g==

View File

@ -0,0 +1,3 @@
U2FsdGVkX19MH3jJZJHEhLZLqIGcQCvd7JS2I8vWztP1Htde6A/xfy3zP8U6NUOc
QPBYfycwXLqUM89gVrKnnj28HQiAzQNf2zzPqG7MOpQKA6zdRF6i9n+CGtvXC36u
zQ==

3
secrets/keys/rnhmjoj.pub Normal file
View File

@ -0,0 +1,3 @@
U2FsdGVkX18X2ltRnCWQnXMSt/FSKiq/ScbhdjFP4wmPHi5njgtam/c1Dg+0T1fj
JzOYe53LglUBfjDMbIepcIymHXPteizligpJzNE7DwuzsCp2JTkn9KWzKJb45Qa/
/UtVdTfkS9WH

10
secrets/misc/asjon.env Normal file
View File

@ -0,0 +1,10 @@
U2FsdGVkX18ufnreJQQJJ52gMxajdK5bLn8A7Gqb3OvqThlWWb5mo4UV+VqEf/ob
VSydFr03zlSYuAuyvpHcunlTHnJR6RPgEdv0qV2NFBaVAlVjqJDgZHPKNLCp2Zws
LOgrWiaRGTKrBAD/80JlzsFyk5YVSXd9fTqo05bTym8qKv39vFrrmZQu1SOKRKmn
qrIUr9MjG25iLCxR6ajcANgfb3+hgQMo5ypr7AwjMp1PwkU/IWf1atIWFJzf0ZU1
4JOsDB4FvuX0hdi8J8LKRe+t0hsjQxb4FS3sMWrSDKhjvjRP+AEEwdj/3YbX856i
l9h2Yd36BtKOrwgrMQTS0pHvnUwj+o/4KeFrteccwgJP5bBJYVts10vg52FldNTg
qTrnnjVrjVm/by8Of435ttSXNmqn5g10MUKKLIIgNZXJcUY/fW4v07xduDHFMUYA
YJWfOfyR4Jlb2lJjmG0VwgPqhVMLAqFrL8XLGlqv1D/nKchktwp58cOqo95js+BT
Q8yvEzMbbtPM4MIGUhzfMbVhXFMmQRgfSpQFAPHe/33V0Ddsp7nCj0n7P+g0b8Ka
2BBS8ez8+7DCyTIerKCwB2+Hu9vy1bkhr8ugZXbmxvL3+fSHgMJ//KWFClQ=

2
secrets/misc/ydns.env Normal file
View File

@ -0,0 +1,2 @@
U2FsdGVkX1+nBNkZvBovUtzVk+hzQxFfQJ2NoORch7iPe33Zf+UIKOqkAWK3hjgb
aYDcTVL3ef1iD4saMpueUpoz36+TtwXAowPzGq0+BVLDyVikU9LM6QBlQQ==

View File

@ -0,0 +1,2 @@
U2FsdGVkX19YVs+neL4R4JDT1CSsndTtbggYoDxEF2iwRCRDJRtrBBJthnxRrUsr
c+A5NSSRRAu0LQ5vjaHlOYiCtmVCdYu7ECrpHQ40KqYgYhXJAw==

3
secrets/pass/root.pwd Normal file
View File

@ -0,0 +1,3 @@
U2FsdGVkX1+1zBjw7Y2NlBeTLcGS8o3Er/ngQMU57HLCN8jSfKBU0/C4o9D4NDjl
C7pRu3oOHmz0Pn9ipLaP87ST9RzVncHw/kqNBh8Dg29n3jNoTdSfwTn6xV/mBwQO
a4OsKusYMI/dCriATixomxe1GkC06YfwJg==

888
secrets/transcrypt Executable file
View File

@ -0,0 +1,888 @@
#!/usr/bin/env nix-shell
#! nix-shell -i bash --pure
#! nix-shell -p bash openssl git unixtools.column
set -euo pipefail
#
# transcrypt - https://github.com/elasticdog/transcrypt
#
# A script to configure transparent encryption of sensitive files stored in
# a Git repository. It utilizes OpenSSL's symmetric cipher routines and follows
# the gitattributes(5) man page regarding the use of filters.
#
# Copyright (c) 2014-2019 Aaron Bull Schaefer <aaron@elasticdog.com>
# This source code is provided under the terms of the MIT License
# that can be be found in the LICENSE file.
#
##### CONSTANTS
# the release version of this script
readonly VERSION='2.0.0'
# the default cipher to utilize
readonly DEFAULT_CIPHER='aes-256-ctr'
# the openssl options to encrypt/decrypt the files
# shellcheck disable=SC2016
readonly ENCRYPT_OPTIONS='-$cipher -pbkdf2 -iter 200000'
# regular expression used to test user input
readonly YES_REGEX='^[Yy]$'
## Repository Metadata
# whether or not transcrypt is already configured
readonly CONFIGURED=$(git config --get --local transcrypt.version 2>/dev/null)
# the current git repository's top-level directory
readonly REPO=$(git rev-parse --show-toplevel 2>/dev/null)
# whether or not a HEAD revision exists
readonly HEAD_EXISTS=$(git rev-parse --verify --quiet HEAD 2>/dev/null)
# https://github.com/RichiH/vcsh
# whether or not the git repository is running under vcsh
readonly IS_VCSH=$(git config --get --local --bool vcsh.vcsh 2>/dev/null)
# whether or not the git repository is bare
readonly IS_BARE=$(git rev-parse --is-bare-repository 2>/dev/null)
## Git Directory Handling
# print a canonicalized absolute pathname
realpath() {
local path=$1
# make path absolute
local abspath=$path
if [[ -n ${abspath##/*} ]]; then
abspath=$(pwd -P)/$abspath
fi
# canonicalize path
local dirname=
if [[ -d $abspath ]]; then
dirname=$(cd "$abspath" && pwd -P)
abspath=$dirname
elif [[ -e $abspath ]]; then
dirname=$(cd "${abspath%/*}/" 2>/dev/null && pwd -P)
abspath=$dirname/${abspath##*/}
fi
if [[ -d $dirname && -e $abspath ]]; then
printf '%s\n' "$abspath"
else
printf 'invalid path: %s\n' "$path" >&2
exit 1
fi
}
# the current git repository's .git directory
RELATIVE_GIT_DIR=$(git rev-parse --git-dir 2>/dev/null)
readonly GIT_DIR=$(realpath "$RELATIVE_GIT_DIR" 2>/dev/null)
# the current git repository's gitattributes file
readonly CORE_ATTRIBUTES=$(git config --get --local --path core.attributesFile)
if [[ $CORE_ATTRIBUTES ]]; then
readonly GIT_ATTRIBUTES=$CORE_ATTRIBUTES
elif [[ $IS_BARE == 'true' ]] || [[ $IS_VCSH == 'true' ]]; then
readonly GIT_ATTRIBUTES="${GIT_DIR}/info/attributes"
else
readonly GIT_ATTRIBUTES="${REPO}/.gitattributes"
fi
##### FUNCTIONS
# print a message to stderr
warn() {
local fmt="$1"
shift
# shellcheck disable=SC2059
printf "transcrypt: $fmt\n" "$@" >&2
}
# print a message to stderr and exit with either
# the given status or that of the most recent command
die() {
local st="$?"
if [[ "$1" != *[^0-9]* ]]; then
st="$1"
shift
fi
warn "$@"
exit "$st"
}
# verify that all requirements have been met
run_safety_checks() {
# validate that we're in a git repository
[[ $GIT_DIR ]] || die 'you are not currently in a git repository; did you forget to run "git init"?'
# exit if transcrypt is not in the required state
if [[ $requires_existing_config ]] && [[ ! $CONFIGURED ]]; then
die 1 'the current repository is not configured'
elif [[ ! $requires_existing_config ]] && [[ $CONFIGURED ]]; then
die 1 'the current repository is already configured; see --display'
fi
# check for dependencies
for cmd in {column,grep,mktemp,openssl,sed,tee}; do
command -v $cmd >/dev/null || die 'required command "%s" was not found' "$cmd"
done
# ensure the repository is clean (if it has a HEAD revision) so we can force
# checkout files without the destruction of uncommitted changes
if [[ $requires_clean_repo ]] && [[ $HEAD_EXISTS ]] && [[ $IS_BARE == 'false' ]]; then
# check if the repo is dirty
if ! git diff-index --quiet HEAD --; then
die 1 'the repo is dirty; commit or stash your changes before running transcrypt'
fi
fi
}
# unset the cipher variable if it is not supported by openssl
validate_cipher() {
local list_cipher_commands
list_cipher_commands='openssl enc -ciphers'
remove_dash() {
sed 's#\(^\| \)-#\1#g'
}
local supported
supported=$($list_cipher_commands | remove_dash | tr -s ' ' '\n' | grep --line-regexp "$cipher") || true
if [[ ! $supported ]]; then
if [[ $interactive ]]; then
printf '"%s" is not a valid cipher; choose one of the following:\n\n' "$cipher"
$list_cipher_commands | remove_dash | column -c 80
printf '\n'
cipher=''
else
# shellcheck disable=SC2016
die 1 '"%s" is not a valid cipher; see `%s`' "$cipher" "$($list_cipher_commands | remove_dash)"
fi
fi
}
# ensure we have a cipher to encrypt with
get_cipher() {
while [[ ! $cipher ]]; do
local answer=
if [[ $interactive ]]; then
printf 'Encrypt using which cipher? [%s] ' "$DEFAULT_CIPHER"
read -r answer
fi
# use the default cipher if the user gave no answer;
# otherwise verify the given cipher is supported by openssl
if [[ ! $answer ]]; then
cipher=$DEFAULT_CIPHER
else
cipher=$answer
validate_cipher
fi
done
}
# ensure we have a password to encrypt with
get_password() {
while [[ ! $password ]]; do
local answer=
if [[ $interactive ]]; then
printf 'Generate a random password? [Y/n] '
read -r -n 1 -s answer
printf '\n'
fi
# generate a random password if the user answered yes;
# otherwise prompt the user for a password
if [[ $answer =~ $YES_REGEX ]] || [[ ! $answer ]]; then
local password_length=30
local random_base64
random_base64=$(openssl rand -base64 $password_length)
password=$random_base64
else
printf 'Password: '
read -r password
[[ $password ]] || printf 'no password was specified\n'
fi
done
}
# confirm the transcrypt configuration
confirm_configuration() {
local answer=
printf '\nRepository metadata:\n\n'
[[ ! $REPO ]] || printf ' GIT_WORK_TREE: %s\n' "$REPO"
printf ' GIT_DIR: %s\n' "$GIT_DIR"
printf ' GIT_ATTRIBUTES: %s\n\n' "$GIT_ATTRIBUTES"
printf 'The following configuration will be saved:\n\n'
printf ' CIPHER: %s\n' "$cipher"
printf ' PASSWORD: %s\n\n' "$password"
printf 'Does this look correct? [Y/n] '
read -r -n 1 -s answer
# exit if the user did not confirm
if [[ $answer =~ $YES_REGEX ]] || [[ ! $answer ]]; then
printf '\n\n'
else
printf '\n'
die 1 'configuration has been aborted'
fi
}
# confirm the rekey configuration
confirm_rekey() {
local answer=
printf '\nRepository metadata:\n\n'
[[ ! $REPO ]] || printf ' GIT_WORK_TREE: %s\n' "$REPO"
printf ' GIT_DIR: %s\n' "$GIT_DIR"
printf ' GIT_ATTRIBUTES: %s\n\n' "$GIT_ATTRIBUTES"
printf 'The following configuration will be saved:\n\n'
printf ' CIPHER: %s\n' "$cipher"
printf ' PASSWORD: %s\n\n' "$password"
printf 'You are about to re-encrypt all encrypted files using new credentials.\n'
printf 'Once you do this, their historical diffs will no longer display in plain text.\n\n'
printf 'Proceed with rekey? [y/N] '
read -r answer
# only rekey if the user explicitly confirmed
if [[ $answer =~ $YES_REGEX ]]; then
printf '\n'
else
die 1 'rekeying has been aborted'
fi
}
# automatically stage rekeyed files in preparation for the user to commit them
stage_rekeyed_files() {
local encrypted_files
encrypted_files=$(git ls-crypt)
if [[ $encrypted_files ]] && [[ $IS_BARE == 'false' ]]; then
# touch all encrypted files to prevent stale stat info
cd "$REPO" || die 1 'could not change into the "%s" directory' "$REPO"
# shellcheck disable=SC2086
touch $encrypted_files
# shellcheck disable=SC2086
git update-index --add -- $encrypted_files
printf '*** rekeyed files have been staged ***\n'
printf '*** COMMIT THESE CHANGES RIGHT AWAY! ***\n\n'
fi
}
# save helper scripts under the repository's git directory
save_helper_scripts() {
mkdir -p "${GIT_DIR}/crypt"
openssl_command="openssl enc $ENCRYPT_OPTIONS -pass env:ENC_PASS"
# The `decryption -> encryption` process on an unchanged file must be
# deterministic for everything to work transparently. To do that, the same
# salt must be used each time we encrypt the same file. An HMAC has been
# proven to be a PRF, so we generate an HMAC-SHA256 for each decrypted file
# (keyed with a combination of the filename and transcrypt password), and
# then use the last 16 bytes of that HMAC for the file's unique salt.
cat <<-'EOF' >"${GIT_DIR}/crypt/clean"
#!/usr/bin/env bash
filename=$1
# ignore empty files
if [[ -s $filename ]]; then
# cache STDIN to test if it's already encrypted
tempfile=$(mktemp 2>/dev/null || mktemp -t tmp)
trap 'rm -f "$tempfile"' EXIT
tee "$tempfile" &>/dev/null
# the first bytes of an encrypted file are always "Salted" in Base64
read -n 8 firstbytes <"$tempfile"
if [[ $firstbytes == "U2FsdGVk" ]]; then
cat "$tempfile"
else
cipher=$(git config --get --local transcrypt.cipher)
password=$(git config --get --local transcrypt.password)
salt=$(openssl dgst -hmac "${filename}:${password}" -sha256 "$filename" | tr -d '\r\n' | tail -c 16)
ENC_PASS=$password @openssl_command@ -e -a -S "$salt" -in "$tempfile"
fi
fi
EOF
cat <<-'EOF' >"${GIT_DIR}/crypt/smudge"
#!/usr/bin/env bash
tempfile=$(mktemp 2>/dev/null || mktemp -t tmp)
trap 'rm -f "$tempfile"' EXIT
cipher=$(git config --get --local transcrypt.cipher)
password=$(git config --get --local transcrypt.password)
tee "$tempfile" | ENC_PASS=$password @openssl_command@ -d -a 2>/dev/null || cat "$tempfile"
EOF
cat <<-'EOF' >"${GIT_DIR}/crypt/textconv"
#!/usr/bin/env bash
filename=$1
# ignore empty files
if [[ -s $filename ]]; then
cipher=$(git config --get --local transcrypt.cipher)
password=$(git config --get --local transcrypt.password)
ENC_PASS=$password @openssl_command@ -d -a -in "$filename" 2>/dev/null || cat "$filename"
fi
EOF
# make scripts executable
for script in {clean,smudge,textconv}; do
chmod 0755 "${GIT_DIR}/crypt/${script}"
sed "s/@openssl_command@/$openssl_command/" -i "${GIT_DIR}/crypt/${script}"
done
}
# write the configuration to the repository's git config
save_configuration() {
save_helper_scripts
# write the encryption info
git config transcrypt.version "$VERSION"
git config transcrypt.cipher "$cipher"
git config transcrypt.password "$password"
# write the filter settings
if [[ -d $(git rev-parse --git-common-dir) ]]; then
# this allows us to support multiple working trees via git-worktree
# ...but the --git-common-dir flag was only added in November 2014
# shellcheck disable=SC2016
git config filter.crypt.clean '"$(git rev-parse --git-common-dir)"/crypt/clean %f'
# shellcheck disable=SC2016
git config filter.crypt.smudge '"$(git rev-parse --git-common-dir)"/crypt/smudge'
# shellcheck disable=SC2016
git config diff.crypt.textconv '"$(git rev-parse --git-common-dir)"/crypt/textconv'
else
# shellcheck disable=SC2016
git config filter.crypt.clean '"$(git rev-parse --git-dir)"/crypt/clean %f'
# shellcheck disable=SC2016
git config filter.crypt.smudge '"$(git rev-parse --git-dir)"/crypt/smudge'
# shellcheck disable=SC2016
git config diff.crypt.textconv '"$(git rev-parse --git-dir)"/crypt/textconv'
fi
git config filter.crypt.required 'true'
git config diff.crypt.cachetextconv 'true'
git config diff.crypt.binary 'true'
git config merge.renormalize 'true'
# add a git alias for listing encrypted files
git config alias.ls-crypt "!git ls-files | git check-attr --stdin filter | awk 'BEGIN { FS = \":\" }; /crypt$/{ print \$1 }'"
}
# display the current configuration settings
display_configuration() {
local current_cipher
current_cipher=$(git config --get --local transcrypt.cipher)
local current_password
current_password=$(git config --get --local transcrypt.password)
local escaped_password=${current_password//\'/\'\\\'\'}
printf 'The current repository was configured using transcrypt version %s\n' "$CONFIGURED"
printf 'and has the following configuration:\n\n'
[[ ! $REPO ]] || printf ' GIT_WORK_TREE: %s\n' "$REPO"
printf ' GIT_DIR: %s\n' "$GIT_DIR"
printf ' GIT_ATTRIBUTES: %s\n\n' "$GIT_ATTRIBUTES"
printf ' CIPHER: %s\n' "$current_cipher"
printf ' PASSWORD: %s\n\n' "$current_password"
printf 'Copy and paste the following command to initialize a cloned repository:\n\n'
printf " transcrypt -c %s -p '%s'\n" "$current_cipher" "$escaped_password"
}
# remove transcrypt-related settings from the repository's git config
clean_gitconfig() {
git config --remove-section transcrypt 2>/dev/null || true
git config --remove-section filter.crypt 2>/dev/null || true
git config --remove-section diff.crypt 2>/dev/null || true
git config --unset merge.renormalize
# remove the merge section if it's now empty
local merge_values
merge_values=$(git config --get-regex --local 'merge\..*') || true
if [[ ! $merge_values ]]; then
git config --remove-section merge 2>/dev/null || true
fi
}
# force the checkout of any files with the crypt filter applied to them;
# this will decrypt existing encrypted files if you've just cloned a repository,
# or it will encrypt locally decrypted files if you've just flushed the credentials
force_checkout() {
# make sure a HEAD revision exists
if [[ $HEAD_EXISTS ]] && [[ $IS_BARE == 'false' ]]; then
# this would normally delete uncommitted changes in the working directory,
# but we already made sure the repo was clean during the safety checks
local encrypted_files
encrypted_files=$(git ls-crypt)
cd "$REPO" || die 1 'could not change into the "%s" directory' "$REPO"
IFS=$'\n'
for file in $encrypted_files; do
rm "$file"
git checkout --force HEAD -- "$file" >/dev/null
done
unset IFS
fi
}
# remove the locally cached encryption credentials and
# re-encrypt any files that had been previously decrypted
flush_credentials() {
local answer=
if [[ $interactive ]]; then
printf 'You are about to flush the local credentials; make sure you have saved them elsewhere.\n'
printf 'All previously decrypted files will revert to their encrypted form.\n\n'
printf 'Proceed with credential flush? [y/N] '
read -r answer
printf '\n'
else
# although destructive, we should support the --yes option
answer='y'
fi
# only flush if the user explicitly confirmed
if [[ $answer =~ $YES_REGEX ]]; then
clean_gitconfig
# re-encrypt any files that had been previously decrypted
force_checkout
printf 'The local transcrypt credentials have been successfully flushed.\n'
else
die 1 'flushing of credentials has been aborted'
fi
}
# remove all transcrypt configuration from the repository
uninstall_transcrypt() {
local answer=
if [[ $interactive ]]; then
printf 'You are about to remove all transcrypt configuration from your repository.\n'
printf 'All previously encrypted files will remain decrypted in this working copy.\n\n'
printf 'Proceed with uninstall? [y/N] '
read -r answer
printf '\n'
else
# although destructive, we should support the --yes option
answer='y'
fi
# only uninstall if the user explicitly confirmed
if [[ $answer =~ $YES_REGEX ]]; then
clean_gitconfig
# remove helper scripts
for script in {clean,smudge,textconv}; do
[[ ! -f "${GIT_DIR}/crypt/${script}" ]] || rm "${GIT_DIR}/crypt/${script}"
done
[[ ! -d "${GIT_DIR}/crypt" ]] || rmdir "${GIT_DIR}/crypt"
# touch all encrypted files to prevent stale stat info
local encrypted_files
encrypted_files=$(git ls-crypt)
if [[ $encrypted_files ]] && [[ $IS_BARE == 'false' ]]; then
cd "$REPO" || die 1 'could not change into the "%s" directory' "$REPO"
# shellcheck disable=SC2086
touch $encrypted_files
fi
# remove the `git ls-crypt` alias
git config --unset alias.ls-crypt
# remove the alias section if it's now empty
local alias_values
alias_values=$(git config --get-regex --local 'alias\..*') || true
if [[ ! $alias_values ]]; then
git config --remove-section alias 2>/dev/null || true
fi
# remove any defined crypt patterns in gitattributes
case $OSTYPE in
darwin*)
/usr/bin/sed -i '' '/filter=crypt diff=crypt[ \t]*$/d' "$GIT_ATTRIBUTES"
;;
linux*)
sed -i '/filter=crypt diff=crypt[ \t]*$/d' "$GIT_ATTRIBUTES"
;;
esac
printf 'The transcrypt configuration has been completely removed from the repository.\n'
else
die 1 'uninstallation has been aborted'
fi
}
# list all of the currently encrypted files in the repository
list_files() {
if [[ $IS_BARE == 'false' ]]; then
cd "$REPO" || die 1 'could not change into the "%s" directory' "$REPO"
git ls-files | git check-attr --stdin filter | awk 'BEGIN { FS = ":" }; /crypt$/{ print $1 }'
fi
}
# show the raw file as stored in the git commit object
show_raw_file() {
if [[ -f $show_file ]]; then
# ensure the file is currently being tracked
local escaped_file=${show_file//\//\\\/}
if git ls-files --others -- "$show_file" | awk "/${escaped_file}/{ exit 1 }"; then
file_paths=$(git ls-tree --name-only --full-name HEAD "$show_file")
else
die 1 'the file "%s" is not currently being tracked by git' "$show_file"
fi
elif [[ $show_file == '*' ]]; then
file_paths=$(git ls-crypt)
else
die 1 'the file "%s" does not exist' "$show_file"
fi
IFS=$'\n'
for file in $file_paths; do
printf '==> %s <==\n' "$file" >&2
git --no-pager show HEAD:"$file" --no-textconv
printf '\n' >&2
done
unset IFS
}
# export password and cipher to a gpg encrypted file
export_gpg() {
# check for dependencies
command -v gpg >/dev/null || die 'required command "gpg" was not found'
# ensure the recipient key exists
if ! gpg --list-keys "$gpg_recipient" 2>/dev/null; then
die 1 'GPG recipient key "%s" does not exist' "$gpg_recipient"
fi
local current_cipher
current_cipher=$(git config --get --local transcrypt.cipher)
local current_password
current_password=$(git config --get --local transcrypt.password)
mkdir -p "${GIT_DIR}/crypt"
local gpg_encrypt_cmd="gpg --batch --recipient $gpg_recipient --trust-model always --yes --armor --quiet --encrypt -"
printf 'password=%s\ncipher=%s\n' "$current_password" "$current_cipher" | $gpg_encrypt_cmd >"${GIT_DIR}/crypt/${gpg_recipient}.asc"
printf "The transcrypt configuration has been encrypted and exported to:\n%s/crypt/%s.asc\n" "$GIT_DIR" "$gpg_recipient"
}
# import password and cipher from a gpg encrypted file
import_gpg() {
# check for dependencies
command -v gpg >/dev/null || die 'required command "gpg" was not found'
local path
if [[ -f "${GIT_DIR}/crypt/${gpg_import_file}" ]]; then
path="${GIT_DIR}/crypt/${gpg_import_file}"
elif [[ -f "${GIT_DIR}/crypt/${gpg_import_file}.asc" ]]; then
path="${GIT_DIR}/crypt/${gpg_import_file}.asc"
elif [[ ! -f $gpg_import_file ]]; then
die 1 'the file "%s" does not exist' "$gpg_import_file"
else
path="$gpg_import_file"
fi
local configuration=''
local safety_counter=0 # fix for intermittent 'no secret key' decryption failures
while [[ ! $configuration ]]; do
configuration=$(gpg --batch --quiet --decrypt "$path")
safety_counter=$((safety_counter + 1))
if [[ $safety_counter -eq 3 ]]; then
die 1 'unable to decrypt the file "%s"' "$path"
fi
done
cipher=$(printf '%s' "$configuration" | grep '^cipher' | cut -d'=' -f 2-)
password=$(printf '%s' "$configuration" | grep '^password' | cut -d'=' -f 2-)
}
# print this script's usage message to stderr
usage() {
cat <<-EOF >&2
usage: transcrypt [-c CIPHER] [-p PASSWORD] [-h]
EOF
}
# print this script's help message to stdout
help() {
cat <<-EOF
NAME
transcrypt -- transparently encrypt files within a git repository
SYNOPSIS
transcrypt [options...]
DESCRIPTION
transcrypt will configure a Git repository to support the transparent
encryption/decryption of files by utilizing OpenSSL's symmetric cipher
routines and Git's built-in clean/smudge filters. It will also add a
Git alias "ls-crypt" to list all transparently encrypted files within
the repository.
The transcrypt source code and full documentation may be downloaded
from https://github.com/elasticdog/transcrypt.
OPTIONS
-c, --cipher=CIPHER
the symmetric cipher to utilize for encryption;
defaults to aes-256-cbc
-p, --password=PASSWORD
the password to derive the key from;
defaults to 30 random base64 characters
-y, --yes
assume yes and accept defaults for non-specified options
-d, --display
display the current repository's cipher and password
-r, --rekey
re-encrypt all encrypted files using new credentials
-f, --flush-credentials
remove the locally cached encryption credentials and re-encrypt
any files that had been previously decrypted
-F, --force
ignore whether the git directory is clean, proceed with the
possibility that uncommitted changes are overwritten
-u, --uninstall
remove all transcrypt configuration from the repository and
leave files in the current working copy decrypted
-l, --list
list all of the transparently encrypted files in the repository,
relative to the top-level directory
-s, --show-raw=FILE
show the raw file as stored in the git commit object; use this
to check if files are encrypted as expected
-e, --export-gpg=RECIPIENT
export the repository's cipher and password to a file encrypted
for a gpg recipient
-i, --import-gpg=FILE
import the password and cipher from a gpg encrypted file
-v, --version
print the version information
-h, --help
view this help message
EXAMPLES
To initialize a Git repository to support transparent encryption, just
change into the repo and run the transcrypt script. transcrypt will
prompt you interactively for all required information if the corre-
sponding option flags were not given.
$ cd <path-to-your-repo>/
$ transcrypt
Once a repository has been configured with transcrypt, you can trans-
parently encrypt files by applying the "crypt" filter and diff to a
pattern in the top-level .gitattributes config. If that pattern matches
a file in your repository, the file will be transparently encrypted
once you stage and commit it:
$ echo 'sensitive_file filter=crypt diff=crypt' >> .gitattributes
$ git add .gitattributes sensitive_file
$ git commit -m 'Add encrypted version of a sensitive file'
See the gitattributes(5) man page for more information.
If you have just cloned a repository containing files that are
encrypted, you'll want to configure transcrypt with the same cipher and
password as the origin repository. Once transcrypt has stored the
matching credentials, it will force a checkout of any existing
encrypted files in order to decrypt them.
If the origin repository has just rekeyed, all clones should flush
their transcrypt credentials, fetch and merge the new encrypted files
via Git, and then re-configure transcrypt with the new credentials.
AUTHOR
Aaron Bull Schaefer <aaron@elasticdog.com>
SEE ALSO
enc(1), gitattributes(5)
EOF
}
##### MAIN
# reset all variables that might be set
cipher=''
password=''
interactive='true'
display_config=''
rekey=''
flush_creds=''
uninstall=''
show_file=''
gpg_recipient=''
gpg_import_file=''
# used to bypass certain safety checks
requires_existing_config=''
requires_clean_repo='true'
# parse command line options
while [[ "${1:-}" != '' ]]; do
case $1 in
-c | --cipher)
cipher=$2
shift
;;
--cipher=*)
cipher=${1#*=}
;;
-p | --password)
password=$2
shift
;;
--password=*)
password=${1#*=}
;;
-y | --yes)
interactive=''
;;
-d | --display)
display_config='true'
requires_existing_config='true'
requires_clean_repo=''
;;
-r | --rekey)
rekey='true'
requires_existing_config='true'
;;
-f | --flush-credentials)
flush_creds='true'
requires_existing_config='true'
;;
-F | --force)
requires_clean_repo=''
;;
-u | --uninstall)
uninstall='true'
requires_existing_config='true'
requires_clean_repo=''
;;
-l | --list)
list_files
exit 0
;;
-s | --show-raw)
show_file=$2
show_raw_file
exit 0
;;
--show-raw=*)
show_file=${1#*=}
show_raw_file
exit 0
;;
-e | --export-gpg)
gpg_recipient=$2
requires_existing_config='true'
requires_clean_repo=''
shift
;;
--export-gpg=*)
gpg_recipient=${1#*=}
requires_existing_config='true'
requires_clean_repo=''
;;
-i | --import-gpg)
gpg_import_file=$2
shift
;;
--import-gpg=*)
gpg_import_file=${1#*=}
;;
-v | --version)
printf 'transcrypt %s\n' "$VERSION"
exit 0
;;
-h | --help | -\?)
help
exit 0
;;
--*)
warn 'unknown option -- %s' "${1#--}"
usage
exit 1
;;
*)
warn 'unknown option -- %s' "${1#-}"
usage
exit 1
;;
esac
shift
done
# always run our safety checks
run_safety_checks
# in order to keep behavior consistent no matter what order the options were
# specified in, we must run these here rather than in the case statement above
if [[ $uninstall ]]; then
uninstall_transcrypt
exit 0
elif [[ $display_config ]] && [[ $flush_creds ]]; then
display_configuration
printf '\n'
flush_credentials
exit 0
elif [[ $display_config ]]; then
display_configuration
exit 0
elif [[ $flush_creds ]]; then
flush_credentials
exit 0
elif [[ $gpg_recipient ]]; then
export_gpg
exit 0
elif [[ $gpg_import_file ]]; then
import_gpg
elif [[ $cipher ]]; then
validate_cipher
fi
# perform function calls to configure transcrypt
get_cipher
get_password
if [[ $rekey ]] && [[ $interactive ]]; then
confirm_rekey
elif [[ $interactive ]]; then
confirm_configuration
fi
save_configuration
if [[ $rekey ]]; then
stage_rekeyed_files
else
force_checkout
fi
# ensure the git attributes file exists
if [[ ! -f $GIT_ATTRIBUTES ]]; then
mkdir -p "${GIT_ATTRIBUTES%/*}"
printf '#pattern filter=crypt diff=crypt\n' >"$GIT_ATTRIBUTES"
fi
printf 'The repository has been successfully configured by transcrypt.\n'
exit 0

18
variables.nix Normal file
View File

@ -0,0 +1,18 @@
{ lib, ... }:
# This file contains global constants that are
# used thoughout the configuration files. They are
# "variables", in the sense that they can change
# from time to time and we don't like to search-replace.
{
options.var = lib.mkOption {
type = lib.types.attrs;
readOnly = true;
default = {
hostname = "maxwell.ydns.eu";
ipAddress = "2.25.5.112";
};
description = "Global constants.";
};
}