about summary refs log tree commit diff
path: root/ops/infra
diff options
context:
space:
mode:
authorVincent Ambo <tazjin@google.com>2019-12-20T20·18+0000
committerVincent Ambo <tazjin@google.com>2019-12-20T20·18+0000
commit03bfe08e1dd9faf48b06cb146bfa446575cde88a (patch)
tree55317968922a9b2a01516f1b79527874df037517 /ops/infra
parente52eed3cd4f73779c2e7c350537fb346835ba9f3 (diff)
chore: Significantly restructure folder layout r/237
This moves the various projects from "type-based" folders (such as
"services" or "tools") into more appropriate semantic folders (such as
"nix", "ops" or "web").

Deprecated projects (nixcon-demo & gotest) which only existed for
testing/demonstration purposes have been removed.

(Note: *all* builds are broken with this commit)
Diffstat (limited to 'ops/infra')
-rwxr-xr-xops/infra/dns/import11
-rw-r--r--ops/infra/dns/kontemplate-works15
-rw-r--r--ops/infra/dns/oslo-pub8
-rw-r--r--ops/infra/dns/root-tazj-in33
-rw-r--r--ops/infra/gcp/.gitignore3
-rw-r--r--ops/infra/gcp/default.tf111
-rw-r--r--ops/infra/kubernetes/cgit/config.yaml73
-rw-r--r--ops/infra/kubernetes/gemma/config.lisp19
-rw-r--r--ops/infra/kubernetes/https-cert/cert.yaml8
-rw-r--r--ops/infra/kubernetes/https-lb/ingress.yaml35
-rw-r--r--ops/infra/kubernetes/nginx/nginx.conf59
-rw-r--r--ops/infra/kubernetes/nginx/nginx.yaml60
-rw-r--r--ops/infra/kubernetes/nixery/config.yaml67
-rw-r--r--ops/infra/kubernetes/nixery/id_nixery.pub1
-rw-r--r--ops/infra/kubernetes/nixery/known_hosts2
-rw-r--r--ops/infra/kubernetes/nixery/secrets.yaml18
-rw-r--r--ops/infra/kubernetes/nixery/ssh_config4
-rw-r--r--ops/infra/kubernetes/primary-cluster.yaml38
-rw-r--r--ops/infra/kubernetes/tazblog/config.yaml34
-rw-r--r--ops/infra/nixos/.gitignore3
-rw-r--r--ops/infra/nixos/README.md23
-rw-r--r--ops/infra/nixos/adho-configuration.nix200
-rw-r--r--ops/infra/nixos/configuration.nix102
-rw-r--r--ops/infra/nixos/default.nix6
-rw-r--r--ops/infra/nixos/desktop.nix82
-rw-r--r--ops/infra/nixos/dotfiles.nix27
-rw-r--r--ops/infra/nixos/dotfiles/alacritty.yml203
-rw-r--r--ops/infra/nixos/dotfiles/config.fish40
-rw-r--r--ops/infra/nixos/dotfiles/msmtprc16
-rw-r--r--ops/infra/nixos/dotfiles/notmuch-config21
-rw-r--r--ops/infra/nixos/dotfiles/offlineimaprc39
-rw-r--r--ops/infra/nixos/dotfiles/tmux.conf14
-rw-r--r--ops/infra/nixos/home.nix75
-rw-r--r--ops/infra/nixos/mail.nix77
-rw-r--r--ops/infra/nixos/packages.nix132
-rw-r--r--ops/infra/nixos/stallo-configuration.nix39
36 files changed, 1698 insertions, 0 deletions
diff --git a/ops/infra/dns/import b/ops/infra/dns/import
new file mode 100755
index 0000000000..e79e426b55
--- /dev/null
+++ b/ops/infra/dns/import
@@ -0,0 +1,11 @@
+#!/bin/sh
+set -ue
+
+# Imports a zone file into a Google Cloud DNS zone of the same name
+readonly ZONE="${1}"
+
+gcloud dns record-sets import "${ZONE}" \
+       --project composite-watch-759 \
+       --zone-file-format \
+       --delete-all-existing \
+       --zone "${ZONE}"
diff --git a/ops/infra/dns/kontemplate-works b/ops/infra/dns/kontemplate-works
new file mode 100644
index 0000000000..326a129d21
--- /dev/null
+++ b/ops/infra/dns/kontemplate-works
@@ -0,0 +1,15 @@
+;;  -*- mode: zone; -*-
+;; Do not delete these
+kontemplate.works. 21600 IN NS ns-cloud-d1.googledomains.com.
+kontemplate.works. 21600 IN NS ns-cloud-d2.googledomains.com.
+kontemplate.works. 21600 IN NS ns-cloud-d3.googledomains.com.
+kontemplate.works. 21600 IN NS ns-cloud-d4.googledomains.com.
+kontemplate.works. 21600 IN SOA ns-cloud-d1.googledomains.com. cloud-dns-hostmaster.google.com. 4 21600 3600 259200 300
+
+;; Github site setup
+kontemplate.works. 60 IN A 185.199.108.153
+kontemplate.works. 60 IN A 185.199.109.153
+kontemplate.works. 60 IN A 185.199.110.153
+kontemplate.works. 60 IN A 185.199.111.153
+
+www.kontemplate.works. 60 IN CNAME tazjin.github.io.
diff --git a/ops/infra/dns/oslo-pub b/ops/infra/dns/oslo-pub
new file mode 100644
index 0000000000..674687484b
--- /dev/null
+++ b/ops/infra/dns/oslo-pub
@@ -0,0 +1,8 @@
+;; Do not delete these
+oslo.pub. 21600 IN NS ns-cloud-c1.googledomains.com.
+oslo.pub. 21600 IN NS ns-cloud-c2.googledomains.com.
+oslo.pub. 21600 IN NS ns-cloud-c3.googledomains.com.
+oslo.pub. 21600 IN NS ns-cloud-c4.googledomains.com.
+oslo.pub. 21600 IN SOA ns-cloud-c1.googledomains.com. cloud-dns-hostmaster.google.com. 4 21600 3600 1209600 300
+
+oslo.pub. 60 IN A 46.21.106.241
diff --git a/ops/infra/dns/root-tazj-in b/ops/infra/dns/root-tazj-in
new file mode 100644
index 0000000000..43db5834a0
--- /dev/null
+++ b/ops/infra/dns/root-tazj-in
@@ -0,0 +1,33 @@
+;; -*- mode: zone; -*-
+;; Do not delete these
+tazj.in. 21600 IN NS ns-cloud-a1.googledomains.com.
+tazj.in. 21600 IN NS ns-cloud-a2.googledomains.com.
+tazj.in. 21600 IN NS ns-cloud-a3.googledomains.com.
+tazj.in. 21600 IN NS ns-cloud-a4.googledomains.com.
+tazj.in. 21600 IN SOA ns-cloud-a1.googledomains.com. cloud-dns-hostmaster.google.com. 123 21600 3600 1209600 300
+
+;; Email setup
+tazj.in. 300 IN MX 1 aspmx.l.google.com.
+tazj.in. 300 IN MX 5 alt1.aspmx.l.google.com.
+tazj.in. 300 IN MX 5 alt2.aspmx.l.google.com.
+tazj.in. 300 IN MX 10 alt3.aspmx.l.google.com.
+tazj.in. 300 IN MX 10 alt4.aspmx.l.google.com.
+tazj.in. 300 IN TXT "v=spf1 include:_spf.google.com ~all"
+google._domainkey.tazj.in. 21600 IN TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA9AphX/WJf8zVXQB5Jk0Ry1MI6ARa6vEyAoJtpjpt9Nbm7XU4qVWFRJm+L0VFd5EZ5YDPJTIZ90lJE3/B8vae2ipnoGbJbj8LaVSzzIPMbWmhPhX3fkLJFdkv7xRDMDn730iYXRlfkgv6GsqbS8vZt7mzxx4mpnePTI323yjRVkwRW8nGVbsmB25ZoG1/0985" "kg4mSYxzWeJ2ozCPFhT4sfMtZMXe/4QEkJz/zkod29KZfFJmLgEaf73WLdBX8kdwbhuh2PYXt/PwzUrRzF5ujVCsSaTZwdRVPErcf+yo4NvedelTjjs8rFVfoJiaDD1q2bQ3w0gDEBWPdC2VP7k9zwIDAQAB"
+
+;; Site verifications
+tazj.in. 3600 IN TXT "keybase-site-verification=gC4kzEmnLzY7F669PjN-pw2Cf__xHqcxQ08Gb-W9dhE"
+tazj.in. 300 IN TXT "google-site-verification=d3_MI1OwD6q2OT42Vvh0I9w2u3Q5KFBu-PieNUE1Fig"
+www.tazj.in. 3600 IN TXT "keybase-site-verification=ER8m_byyqAhzeIy9TyzkAU1H2p2yHtpvImuB_XrRF2U"
+
+;; Blog "storage engine"
+blog.tazj.in. 21600 IN NS ns-cloud-c1.googledomains.com.
+blog.tazj.in. 21600 IN NS ns-cloud-c2.googledomains.com.
+blog.tazj.in. 21600 IN NS ns-cloud-c3.googledomains.com.
+blog.tazj.in. 21600 IN NS ns-cloud-c4.googledomains.com.
+
+;; Webpage records setup
+tazj.in.       300 IN A 34.98.120.189
+www.tazj.in.   300 IN A 34.98.120.189
+git.tazj.in.   300 IN A 34.98.120.189
+files.tazj.in. 300 IN CNAME c.storage.googleapis.com.
diff --git a/ops/infra/gcp/.gitignore b/ops/infra/gcp/.gitignore
new file mode 100644
index 0000000000..96c7538dda
--- /dev/null
+++ b/ops/infra/gcp/.gitignore
@@ -0,0 +1,3 @@
+.terraform
+*.tfstate
+*.tfstate.backup
diff --git a/ops/infra/gcp/default.tf b/ops/infra/gcp/default.tf
new file mode 100644
index 0000000000..2cb57836fa
--- /dev/null
+++ b/ops/infra/gcp/default.tf
@@ -0,0 +1,111 @@
+# Terraform configuration for the GCP project 'tazjins-infrastructure'
+
+provider "google" {
+  project = "tazjins-infrastructure"
+  region  = "europe-north1"
+}
+
+# Configure a storage bucket in which to keep Terraform state and
+# other data, such as Nixery's layers.
+resource "google_storage_bucket" "tazjins-data" {
+  name     = "tazjins-data"
+  location = "EU"
+}
+
+terraform {
+  backend "gcs" {
+    bucket = "tazjins-data"
+    prefix = "terraform"
+  }
+}
+
+# Configure enabled APIs
+resource "google_project_services" "primary" {
+  project = "tazjins-infrastructure"
+  services = [
+    "bigquery-json.googleapis.com",
+    "bigquerystorage.googleapis.com",
+    "cloudapis.googleapis.com",
+    "clouddebugger.googleapis.com",
+    "cloudfunctions.googleapis.com",
+    "cloudkms.googleapis.com",
+    "cloudtrace.googleapis.com",
+    "compute.googleapis.com",
+    "container.googleapis.com",
+    "containerregistry.googleapis.com",
+    "datastore.googleapis.com",
+    "dns.googleapis.com",
+    "iam.googleapis.com",
+    "iamcredentials.googleapis.com",
+    "logging.googleapis.com",
+    "monitoring.googleapis.com",
+    "oslogin.googleapis.com",
+    "pubsub.googleapis.com",
+    "run.googleapis.com",
+    "servicemanagement.googleapis.com",
+    "serviceusage.googleapis.com",
+    "sourcerepo.googleapis.com",
+    "sql-component.googleapis.com",
+    "storage-api.googleapis.com",
+    "storage-component.googleapis.com",
+  ]
+}
+
+
+# Configure the main Kubernetes cluster in which services are deployed
+resource "google_container_cluster" "primary" {
+  name     = "tazjin-cluster"
+  location = "europe-north1"
+
+  remove_default_node_pool = true
+  initial_node_count       = 1
+}
+
+resource "google_container_node_pool" "primary_nodes" {
+  name       = "primary-nodes"
+  location   = "europe-north1"
+  cluster    = google_container_cluster.primary.name
+  node_count = 1
+
+  node_config {
+    preemptible  = true
+    machine_type = "n1-standard-2"
+
+    oauth_scopes = [
+      "storage-rw",
+      "logging-write",
+      "monitoring",
+      "https://www.googleapis.com/auth/source.read_only",
+    ]
+  }
+}
+
+# Configure a service account for which GCS URL signing keys can be created.
+resource "google_service_account" "nixery" {
+  account_id   = "nixery"
+  display_name = "Nixery service account"
+}
+
+# Configure Cloud KMS for secret encryption
+resource "google_kms_key_ring" "tazjins_keys" {
+  name     = "tazjins-keys"
+  location = "europe-north1"
+
+  lifecycle {
+    prevent_destroy = true
+  }
+}
+
+resource "google_kms_crypto_key" "kontemplate_key" {
+  name     = "kontemplate-key"
+  key_ring = google_kms_key_ring.tazjins_keys.id
+
+  lifecycle {
+    prevent_destroy = true
+  }
+}
+
+# Configure the git repository that contains everything.
+resource "google_sourcerepo_repository" "depot" {
+  name = "depot"
+}
diff --git a/ops/infra/kubernetes/cgit/config.yaml b/ops/infra/kubernetes/cgit/config.yaml
new file mode 100644
index 0000000000..78de82ee09
--- /dev/null
+++ b/ops/infra/kubernetes/cgit/config.yaml
@@ -0,0 +1,73 @@
+---
+apiVersion: v1
+kind: Secret
+metadata:
+  name: gcsr-secrets
+type: Opaque
+data:
+  username: "Z2l0LXRhemppbi5nbWFpbC5jb20="
+  # This credential is a GCSR 'gitcookie' token.
+  password: '{{ passLookup "gcsr-tazjin-password" | b64enc }}'
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: cgit
+  labels:
+    app: cgit
+spec:
+  replicas: 2
+  selector:
+    matchLabels:
+      app: cgit
+  template:
+    metadata:
+      labels:
+        app: cgit
+    spec:
+      securityContext:
+        runAsUser: 1000
+        runAsGroup: 1000
+        fsGroup: 1000
+      containers:
+      - name: cgit
+        image: nixery.local/shell/services.cgit-taz:{{ gitHEAD }}
+        command: [ "cgit-launch" ]
+        env:
+          - name: HOME
+            value: /git
+        volumeMounts:
+          - name: git-volume
+            mountPath: /git
+      - name: sync-gcsr
+        image: nixery.local/shell/services.sync-gcsr:{{ gitHEAD }}
+        command: [ "sync-gcsr" ]
+        env:
+          - name: SYNC_USER
+            valueFrom:
+              secretKeyRef:
+                name: gcsr-secrets
+                key: username
+          - name: SYNC_PASS
+            valueFrom:
+              secretKeyRef:
+                name: gcsr-secrets
+                key: password
+        volumeMounts:
+          - name: git-volume
+            mountPath: /git
+      volumes:
+        - name: git-volume
+          emptyDir: {}
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: cgit
+spec:
+  selector:
+    app: cgit
+  ports:
+    - protocol: TCP
+      port: 80
+      targetPort: 8080
diff --git a/ops/infra/kubernetes/gemma/config.lisp b/ops/infra/kubernetes/gemma/config.lisp
new file mode 100644
index 0000000000..517a658cf1
--- /dev/null
+++ b/ops/infra/kubernetes/gemma/config.lisp
@@ -0,0 +1,19 @@
+(config :port 4242
+        :data-dir "/var/lib/gemma/")
+
+(deftask bathroom/wipe-mirror 7)
+(deftask bathroom/wipe-counter 7)
+
+;; Bedroom tasks
+(deftask bedroom/change-sheets 7)
+(deftask bedroom/vacuum 10)
+
+;; Kitchen tasks
+(deftask kitchen/normal-trash 3)
+(deftask kitchen/green-trash 5)
+(deftask kitchen/blue-trash 5)
+(deftask kitchen/wipe-counters 3)
+(deftask kitchen/vacuum 5 "Kitchen has more crumbs and such!")
+
+;; Entire place
+(deftask clean-windows 60)
diff --git a/ops/infra/kubernetes/https-cert/cert.yaml b/ops/infra/kubernetes/https-cert/cert.yaml
new file mode 100644
index 0000000000..c7a85275ae
--- /dev/null
+++ b/ops/infra/kubernetes/https-cert/cert.yaml
@@ -0,0 +1,8 @@
+---
+apiVersion: networking.gke.io/v1beta1
+kind: ManagedCertificate
+metadata:
+  name: {{ .domain | replace "." "-" }}
+spec:
+  domains:
+    - {{ .domain }}
diff --git a/ops/infra/kubernetes/https-lb/ingress.yaml b/ops/infra/kubernetes/https-lb/ingress.yaml
new file mode 100644
index 0000000000..069771a421
--- /dev/null
+++ b/ops/infra/kubernetes/https-lb/ingress.yaml
@@ -0,0 +1,35 @@
+# This resource configures the HTTPS load balancer that is used as the
+# entrypoint to all HTTPS services running in the cluster.
+---
+apiVersion: extensions/v1beta1
+kind: Ingress
+metadata:
+  name: https-ingress
+  annotations:
+    networking.gke.io/managed-certificates: tazj-in, git-tazj-in, www-tazj-in, oslo-pub
+spec:
+  rules:
+    # Route blog to the blog ...
+    - host: tazj.in
+      http:
+        paths:
+          - path: /*
+            backend:
+              serviceName: tazblog
+              servicePort: 8000
+    # Route git.tazj.in to the cgit pods
+    - host: git.tazj.in
+      http:
+        paths:
+          - path: /*
+            backend:
+              serviceName: nginx
+              servicePort: 6756
+    # Route oslo.pub to the nginx instance which serves redirects
+    - host: oslo.pub
+      http:
+        paths:
+          - path: /
+            backend:
+              serviceName: nginx
+              servicePort: 6756
diff --git a/ops/infra/kubernetes/nginx/nginx.conf b/ops/infra/kubernetes/nginx/nginx.conf
new file mode 100644
index 0000000000..918aa60678
--- /dev/null
+++ b/ops/infra/kubernetes/nginx/nginx.conf
@@ -0,0 +1,59 @@
+daemon off;
+worker_processes  1;
+error_log stderr;
+pid /run/nginx.pid;
+
+events {
+    worker_connections  1024;
+}
+
+http {
+    log_format json_combined escape=json
+    '{'
+        '"time_local":"$time_local",'
+        '"remote_addr":"$remote_addr",'
+        '"remote_user":"$remote_user",'
+        '"request":"$request",'
+        '"status": "$status",'
+        '"body_bytes_sent":"$body_bytes_sent",'
+        '"request_time":"$request_time",'
+        '"http_referrer":"$http_referer",'
+        '"http_user_agent":"$http_user_agent"'
+        '}';
+
+    access_log /dev/stdout json_combined;
+
+    sendfile        on;
+    keepalive_timeout  65;
+
+    server {
+        listen 80 default_server;
+        location / {
+            return 200 "ok";
+        }
+    }
+
+    server {
+        listen       80;
+        server_name  oslo.pub;
+
+        location / {
+            return 302 https://www.google.com/maps/d/viewer?mid=1pJIYY9cuEdt9DuMTbb4etBVq7hs;
+        }
+    }
+
+    server {
+        listen       80;
+        server_name  git.tazj.in;
+
+        # Static assets must always hit the root.
+        location ~ ^/(favicon\.ico|cgit\.(css|png))$ {
+           proxy_pass http://cgit;
+        }
+
+        # Everything else hits the depot directly.
+        location / {
+            proxy_pass http://cgit/cgit.cgi/depot/;
+        }
+    }
+}
diff --git a/ops/infra/kubernetes/nginx/nginx.yaml b/ops/infra/kubernetes/nginx/nginx.yaml
new file mode 100644
index 0000000000..983b265baf
--- /dev/null
+++ b/ops/infra/kubernetes/nginx/nginx.yaml
@@ -0,0 +1,60 @@
+# Deploy an nginx instance which serves ... redirects.
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: nginx-conf
+data:
+  nginx.conf: {{ insertFile "nginx.conf" | toJson }}
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: nginx
+  labels:
+    app: nginx
+spec:
+  replicas: 2
+  selector:
+    matchLabels:
+      app: nginx
+  template:
+    metadata:
+      labels:
+        app: nginx
+        config: {{ insertFile "nginx.conf" | sha1sum }}
+    spec:
+      containers:
+        - name: tazblog
+          image: nixery.local/shell/third_party.nginx:{{ .version }}
+          command: ["/bin/bash", "-c"]
+          args:
+            - |
+              cd /run
+              echo 'nogroup:x:30000:nobody' >> /etc/group
+              echo 'nobody:x:30000:30000:nobody:/tmp:/bin/bash' >> /etc/passwd
+              exec nginx -c /etc/nginx/nginx.conf
+          volumeMounts:
+            - name: nginx-conf
+              mountPath: /etc/nginx
+            - name: nginx-rundir
+              mountPath: /run
+      volumes:
+        - name: nginx-conf
+          configMap:
+            name: nginx-conf
+        - name: nginx-rundir
+          emptyDir: {}
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: nginx
+spec:
+  type: NodePort
+  selector:
+    app: nginx
+  ports:
+    - protocol: TCP
+      port: 6756
+      targetPort: 80
diff --git a/ops/infra/kubernetes/nixery/config.yaml b/ops/infra/kubernetes/nixery/config.yaml
new file mode 100644
index 0000000000..0775e79b58
--- /dev/null
+++ b/ops/infra/kubernetes/nixery/config.yaml
@@ -0,0 +1,67 @@
+# Deploys an instance of Nixery into the cluster.
+#
+# The service via which Nixery is exposed has a private DNS entry
+# pointing to it, which makes it possible to resolve `nixery.local`
+# in-cluster without things getting nasty.
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: nixery
+  namespace: kube-public
+  labels:
+    app: nixery
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: nixery
+  template:
+    metadata:
+      labels:
+        app: nixery
+    spec:
+      containers:
+      - name: nixery
+        image: eu.gcr.io/tazjins-infrastructure/nixery:{{ .version }}
+        volumeMounts:
+          - name: nixery-secrets
+            mountPath: /var/nixery
+        env:
+          - name: BUCKET
+            value: {{ .bucket}}
+          - name: PORT
+            value: "{{ .port }}"
+          - name: GOOGLE_APPLICATION_CREDENTIALS
+            value: /var/nixery/gcs-key.json
+          - name: GCS_SIGNING_KEY
+            value: /var/nixery/gcs-key.pem
+          - name: GCS_SIGNING_ACCOUNT
+            value: {{ .account }}
+          - name: GIT_SSH_COMMAND
+            value: 'ssh -F /var/nixery/ssh_config'
+          - name: NIXERY_PKGS_REPO
+            value: {{ .repo }}
+          - name: NIX_POPULARITY_URL
+            value: 'https://storage.googleapis.com/nixery-layers/popularity/{{ .popularity }}'
+      volumes:
+        - name: nixery-secrets
+          secret:
+            secretName: nixery-secrets
+            defaultMode: 256
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: nixery
+  namespace: kube-public
+  annotations:
+    cloud.google.com/load-balancer-type: "Internal"
+spec:
+  selector:
+    app: nixery
+  type: LoadBalancer
+  ports:
+  - protocol: TCP
+    port: 80
+    targetPort: 8080
diff --git a/ops/infra/kubernetes/nixery/id_nixery.pub b/ops/infra/kubernetes/nixery/id_nixery.pub
new file mode 100644
index 0000000000..dc3fd617d0
--- /dev/null
+++ b/ops/infra/kubernetes/nixery/id_nixery.pub
@@ -0,0 +1 @@
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCzBM6ydst77jDHNcTFWKD9Fw4SReqyNEEp2MtQBk2wt94U4yLp8MQIuNeOEn1GaDEX4RGCxqai/2UVF1w9ZNdU+v2fXcKWfkKuGQH2XcNfXor2cVNObd40H78++iZiv3nmM/NaEdkTbTBbi925cRy9u5FgItDgsJlyKNRglCb0fr6KlgpvWjL20dp/eeZ8a/gLniHK8PnEsgERQSvJnsyFpxxVhxtoUiyLWpXDl4npf/rQr0eRDf4Q5sN/nbTwksapPHfze8dKcaoA7A2NqT3bJ6DPGrwVCzGRtGw/SXJwFwmmtAl9O6BklpeReyiknSxc+KOtrjDW6O0r6yvymD5Z nixery
diff --git a/ops/infra/kubernetes/nixery/known_hosts b/ops/infra/kubernetes/nixery/known_hosts
new file mode 100644
index 0000000000..6a2f84b5fb
--- /dev/null
+++ b/ops/infra/kubernetes/nixery/known_hosts
@@ -0,0 +1,2 @@
+github.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==
+140.82.118.4 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==
diff --git a/ops/infra/kubernetes/nixery/secrets.yaml b/ops/infra/kubernetes/nixery/secrets.yaml
new file mode 100644
index 0000000000..d9a674d2c9
--- /dev/null
+++ b/ops/infra/kubernetes/nixery/secrets.yaml
@@ -0,0 +1,18 @@
+# The secrets below are encrypted using keys stored in Cloud KMS and
+# templated in by kontemplate when deploying.
+#
+# Not all of the values are actually secret (see the matching)
+---
+apiVersion: v1
+kind: Secret
+metadata:
+  name: nixery-secrets
+  namespace: kube-public
+type: Opaque
+data:
+  gcs-key.json: {{ passLookup "nixery-gcs-json" | b64enc }}
+  gcs-key.pem: {{ passLookup "nixery-gcs-pem" | b64enc }}
+  id_nixery: {{ printf "%s\n" (passLookup "nixery-ssh-private") | b64enc }}
+  id_nixery.pub: {{ insertFile "id_nixery.pub" | b64enc }}
+  known_hosts: {{ insertFile "known_hosts" | b64enc }}
+  ssh_config: {{ insertFile "ssh_config" | b64enc }}
diff --git a/ops/infra/kubernetes/nixery/ssh_config b/ops/infra/kubernetes/nixery/ssh_config
new file mode 100644
index 0000000000..78afbb0b03
--- /dev/null
+++ b/ops/infra/kubernetes/nixery/ssh_config
@@ -0,0 +1,4 @@
+Match host *
+      User tazjin@google.com
+      IdentityFile /var/nixery/id_nixery
+      UserKnownHostsFile /var/nixery/known_hosts
diff --git a/ops/infra/kubernetes/primary-cluster.yaml b/ops/infra/kubernetes/primary-cluster.yaml
new file mode 100644
index 0000000000..1d5d33e0bb
--- /dev/null
+++ b/ops/infra/kubernetes/primary-cluster.yaml
@@ -0,0 +1,38 @@
+# Kontemplate configuration for the primary GKE cluster in the project
+# 'tazjins-infrastructure'.
+---
+context: gke_tazjins-infrastructure_europe-north1_tazjin-cluster
+include:
+  # SSL certificates (provisioned by Google)
+  - name: tazj-in-cert
+    path: https-cert
+    values:
+      domain: tazj.in
+  - name: www-tazj-in-cert
+    path: https-cert
+    values:
+      domain: www.tazj.in
+  - name: git-tazj-in-cert
+    path: https-cert
+    values:
+      domain: git.tazj.in
+  - name: oslo-pub-cert
+    path: https-cert
+    values:
+      domain: oslo.pub
+
+  # Services
+  - name: nixery
+    values:
+      port: 8080
+      version: xkm36vrbcnzxdccybzdrx4qzfcfqfrhg
+      bucket: tazjins-data
+      account: nixery@tazjins-infrastructure.iam.gserviceaccount.com
+      repo: ssh://tazjin@gmail.com@source.developers.google.com:2022/p/tazjins-infrastructure/r/depot
+      popularity: 'popularity-nixos-unstable-3140fa89c51233397f496f49014f6b23216667c2.json'
+  - name: tazblog
+  - name: cgit
+  - name: https-lb
+  - name: nginx
+    values:
+      version: a349d5e9145ae9a6c89f62ec631f01fb180de546
diff --git a/ops/infra/kubernetes/tazblog/config.yaml b/ops/infra/kubernetes/tazblog/config.yaml
new file mode 100644
index 0000000000..165a30f683
--- /dev/null
+++ b/ops/infra/kubernetes/tazblog/config.yaml
@@ -0,0 +1,34 @@
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: tazblog
+  labels:
+    app: tazblog
+spec:
+  replicas: 2
+  selector:
+    matchLabels:
+      app: tazblog
+  template:
+    metadata:
+      labels:
+        app: tazblog
+    spec:
+      containers:
+      - name: tazblog
+        image: nixery.local/shell/services.tazblog:{{ gitHEAD }}
+        command: [ "tazblog" ]
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: tazblog
+spec:
+  type: NodePort
+  selector:
+    app: tazblog
+  ports:
+    - protocol: TCP
+      port: 8000
+      targetPort: 8000
diff --git a/ops/infra/nixos/.gitignore b/ops/infra/nixos/.gitignore
new file mode 100644
index 0000000000..773fa16670
--- /dev/null
+++ b/ops/infra/nixos/.gitignore
@@ -0,0 +1,3 @@
+hardware-configuration.nix
+local-configuration.nix
+result
diff --git a/ops/infra/nixos/README.md b/ops/infra/nixos/README.md
new file mode 100644
index 0000000000..4f2e870f90
--- /dev/null
+++ b/ops/infra/nixos/README.md
@@ -0,0 +1,23 @@
+NixOS configuration
+===================
+
+My NixOS configuration! It configures most of the packages I require
+on my systems, sets up Emacs the way I need and does a bunch of other
+interesting things.
+
+In contrast with earlier versions of this configuration, the Nix
+channel versions are now pinned in Nix (see the beginning of
+[packages.nix][]).
+
+Machine-local configuration is kept in files with the naming scheme
+`$hostname-configuration.nix` and **must** be symlinked to
+`local-configuration.nix` before the first configuration run.
+
+I'm publishing this repository (and my [emacs configuration][]) as a
+convenience for myself, but also as a resource that people looking for
+example Nix or Emacs configurations can browse through.
+
+Feel free to ping me with any questions you might have.
+
+[packages.nix]: packages.nix
+[emacs configuration]: https://github.com/tazjin/emacs.d
diff --git a/ops/infra/nixos/adho-configuration.nix b/ops/infra/nixos/adho-configuration.nix
new file mode 100644
index 0000000000..11c5a97df5
--- /dev/null
+++ b/ops/infra/nixos/adho-configuration.nix
@@ -0,0 +1,200 @@
+# Local configuration for 'adho' (Thinkpad T470s)
+{ config, pkgs, ...}:
+
+{
+  boot.initrd.luks.devices.adho.device = "/dev/disk/by-uuid/722006b0-9654-4ea1-8703-e0cf9ac1905e";
+  boot.kernelModules = [ "kvm-intel" ];
+
+  services.xserver.libinput.enable = true;
+  services.xserver.videoDrivers = [ "intel" ];
+  programs.light.enable = true;
+
+  # Office printer configuration
+  services.printing.enable  = true;
+  services.printing.drivers = [ pkgs.hplip ];
+  services.avahi.enable     = true;
+  services.avahi.nssmdns    = true;
+
+  # Enable VirtualBox to update Beatstep Pro firmware:
+  virtualisation.virtualbox.host.enable = true;
+  virtualisation.virtualbox.host.enableExtensionPack = true;
+
+  # Enable LXC/LXD for Nixini work
+  virtualisation.lxd.enable = true;
+
+  # Give me more entropy:
+  services.haveged.enable = true;
+
+  # Disable sandbox to let work-builds function:
+  nix.useSandbox = false;
+
+  # Yubikey related:
+  services.pcscd.enable = true;
+  environment.systemPackages = with pkgs; [
+    cfssl
+    libp11
+    opensc
+    yubico-piv-tool
+  ];
+
+  networking = {
+    hostName = "adho";
+    wireless.enable = true;
+    wireless.userControlled.enable = true;
+
+    wireless.networks = {
+      # Welcome to roast club!
+      "How do I computer?" = {
+        psk = "washyourface";
+      };
+
+      # On the go!
+      "Rumpetroll" = {
+        psk = "fisk1234";
+        # If this network exists, chances are that I want it:
+        priority = 10;
+      };
+
+      # Public places in Oslo:
+      "Abelone" = {
+        psk = "speakeasy";
+      };
+
+      "Wurst" = {
+        psk = "wurst2015";
+      };
+
+      "postkontoret" = {
+        psk = "postkontoret";
+      };
+
+      # Eugene's apartment:
+      "GET_5G_4FD250" = {
+        psk = "62636342";
+      };
+
+      # FSCONS 2017
+      "uioguest" = {};
+
+      # Hackeriet!
+      "hackeriet.no" = {
+        psk = "hackeriet.no";
+      };
+
+      # Cafe Sara
+      "Sara Nett" = {
+        psk = "sarabar1989";
+      };
+
+      # The Dubliner
+      "DubGjest" = {
+        # of course
+        psk = "Guinness";
+      };
+
+      "MAGNAT Guest" = {
+        psk = "elmolino021";
+      };
+
+      "BrewDog" = {
+        psk = "welovebeer";
+      };
+
+      # Dima's
+      "What's a Bad Idea?" = {
+        psk = "DQDxzrzIvy0YtDwH";
+      };
+
+      # Loke's
+      "VMC28F76E" = {
+        psk = "d2ftQnr6xppw";
+      };
+
+      "SafetyWiFi - Teknologihuset" = {
+        psk = "tech4ever";
+      };
+
+      "Selvaag Pluss" = {
+        psk = "detlilleekstra";
+      };
+
+      "Langler" = {
+        psk = "Oslo2018";
+      };
+
+      # Pils & Programmering
+      "BEKKguest" = {
+        psk = "guest7890";
+      };
+
+      "Homan-Gjest" = {
+        psk = "haveaniceday";
+      };
+
+      # Røverstaden
+      "Roverstaden" = {
+        psk = "r0verstaden2018";
+      };
+
+      "The Brew Dock" = {
+        psk = "realbeer";
+      };
+
+      "econ-guest" = {
+        psk = "Finance2010";
+      };
+
+      "KabelBox-2FD0" = {
+        psk = "92433048597489095671";
+      };
+
+      "TheKasbah" = {
+        psk = "couscous";
+      };
+
+      # Kitty's misspelled network.
+      "How do I Computer?" = {
+        psk = "herpderpponies";
+      };
+
+      # NixCon 2018
+      "Coin Street Community Builders " = {
+        psk = "3vents2016";
+      };
+
+      "KH2 Gjest" = {
+        psk = "haenfindag";
+      };
+
+      # Forest & Brown
+      "Forest Guest" = {
+        psk = "437B99AC5B";
+      };
+
+      "Gatwick FREE Wi-Fi" = {};
+      "mycloud" = {};
+      "Norwegian Internet Access" = {};
+      "NSB_INTERAKTIV" = {};
+      "The Thief" = {};
+      "espressohouse" = {};
+      "Gotanet Open" = {};
+      "wifi.flytoget.no" = {};
+      "AIRPORT" = {};
+      "ilcaffelovesyou" = {};
+      "WIFIonICE" = {};
+      "Lorry Gjest" = {};
+      "Amundsengjest" = {};
+      "Beer Palace Gjest" = {};
+      "ibis" = {};
+      "GoogleGuest" = {};
+    };
+  };
+
+  hardware.bluetooth.enable = true;
+
+  # Configure POSIX queue limits (for work)
+  systemd.tmpfiles.rules = let mqueue = "/proc/sys/fs/mqueue"; in [
+    "w ${mqueue}/msgsize_max - - - - ${toString (64 * 1024)}"
+    "w ${mqueue}/msg_max     - - - - 50"
+  ];
+}
diff --git a/ops/infra/nixos/configuration.nix b/ops/infra/nixos/configuration.nix
new file mode 100644
index 0000000000..57f5c56dc7
--- /dev/null
+++ b/ops/infra/nixos/configuration.nix
@@ -0,0 +1,102 @@
+# Edit this configuration file to define what should be installed on
+# your system.  Help is available in the configuration.nix(5) man page
+# and in the NixOS manual (accessible by running ‘nixos-help’).
+
+{ config, lib, pkgs, ... }:
+
+{
+  imports =
+    [
+    ./desktop.nix
+    ./dotfiles.nix
+    ./hardware-configuration.nix
+    ./local-configuration.nix
+    ./mail.nix
+    ./packages.nix
+    ];
+
+  # Use the systemd-boot EFI boot loader.
+  boot.loader.systemd-boot.enable = true;
+  boot.loader.efi.canTouchEfiVariables = true;
+  boot.cleanTmpDir = true;
+  hardware.pulseaudio.enable = true;
+  time.timeZone = "Europe/Oslo";
+
+  # Configure audio setup for JACK + Overtone
+  boot.kernelModules = [ "snd-seq" "snd-rawmidi" ];
+  hardware.pulseaudio.package = pkgs.pulseaudioFull;
+
+  # Update Intel microcode on boot (both machines have Intel CPUs):
+  hardware.cpu.intel.updateMicrocode = true;
+
+  networking = {
+    # Don't use ISP's DNS servers:
+    nameservers = [
+      "1.1.1.1"
+      "1.0.0.1"
+    ];
+
+    # Open Chromecast-related ports & servedir
+    firewall.allowedTCPPorts = [ 3000 5556 5558 ];
+  };
+
+  # Generate an immutable /etc/resolv.conf from the nameserver settings
+  # above (otherwise DHCP overwrites it):
+  environment.etc."resolv.conf" = with lib; with pkgs; {
+    source = writeText "resolv.conf" ''
+      ${concatStringsSep "\n" (map (ns: "nameserver ${ns}") config.networking.nameservers)}
+      options edns0
+    '';
+  };
+
+  # Configure emacs:
+  # (actually, that's a lie, this only installs emacs!)
+  services.emacs = {
+    install = true;
+    defaultEditor = true;
+    package = import ./emacs.nix { inherit pkgs; };
+  };
+
+  services.openssh.enable = true;
+
+  # Enable GNOME keyring (required for Evolution)
+  services.gnome3.gnome-keyring.enable = true;
+
+  virtualisation = {
+    # Configure Docker (with socket activation):
+    # Side note: ... why is this in virtualisation? ...
+    docker.enable = true;
+    docker.autoPrune.enable = true;
+  };
+
+  # Configure various other applications:
+  programs = {
+    java.enable = true;
+    java.package = pkgs.openjdk;
+
+    fish.enable = true;
+    ssh.startAgent = true;
+  };
+
+  services.postgresql.enable = true;
+
+  # Configure user account
+  users.defaultUserShell = pkgs.fish;
+  users.extraUsers.vincent = {
+    extraGroups = [ "wheel" "docker" "vboxusers" "lxd" ];
+    isNormalUser = true;
+    uid = 1000;
+    shell = pkgs.fish;
+  };
+
+  security.sudo = {
+    enable = true;
+    extraConfig = "wheel ALL=(ALL:ALL) SETENV: ALL";
+  };
+
+  # This value determines the NixOS release with which your system is to be
+  # compatible, in order to avoid breaking some software such as database
+  # servers. You should change this only after NixOS release notes say you
+  # should.
+  system.stateVersion = "18.03"; # Did you read the comment?
+}
diff --git a/ops/infra/nixos/default.nix b/ops/infra/nixos/default.nix
new file mode 100644
index 0000000000..42594f6f58
--- /dev/null
+++ b/ops/infra/nixos/default.nix
@@ -0,0 +1,6 @@
+{ ... }:
+
+builtins.throw ''
+  The Nix derivations at infra/nixos are not meant to be evaluated
+  like a derivation as they represent NixOS configuration.
+''
diff --git a/ops/infra/nixos/desktop.nix b/ops/infra/nixos/desktop.nix
new file mode 100644
index 0000000000..07a6274a56
--- /dev/null
+++ b/ops/infra/nixos/desktop.nix
@@ -0,0 +1,82 @@
+# Configuration for the desktop environment
+
+{ config, lib, pkgs, ... }:
+
+let emacs = import ./emacs.nix { inherit pkgs; };
+screenLock = pkgs.writeShellScriptBin "screen-lock" ''
+  find ${pkgs.wallpapers} -name "*.png" | shuf -n1 | xargs i3lock -f -t -i
+'';
+in {
+  # Configure basic X-server stuff:
+  services.xserver = {
+    enable = true;
+    layout = "us,no";
+    xkbOptions = "caps:super, grp:shifts_toggle, parens:swap_brackets";
+    exportConfiguration = true;
+
+    # Give EXWM permission to control the session.
+    displayManager.sessionCommands = "${pkgs.xorg.xhost}/bin/xhost +SI:localuser:$USER";
+
+    # Use the pre 18.09 default display manager (slim)
+    displayManager.slim.enable = true;
+  };
+
+  # Add a shell script with random screen lock wallpaper selection
+  environment.systemPackages = [ screenLock ];
+
+  # Apparently when you have house guests they complain about your screen tearing!
+  services.compton.enable = true;
+  services.compton.backend = "xrender";
+
+  # Configure desktop environment:
+  services.xserver.windowManager.session = lib.singleton {
+    name = "exwm";
+    start = ''
+      ${emacs}/bin/emacs --eval '(progn (server-start) (exwm-enable))'
+    '';
+  };
+
+  # Configure Redshift for Oslo
+  services.redshift = {
+    enable = true;
+    latitude = "59.911491";
+    longitude = "10.757933";
+  };
+
+  # Configure fonts
+  fonts = {
+    fonts = with pkgs; [
+      corefonts
+      font-awesome-ttf
+      input-fonts
+      noto-fonts-cjk
+      noto-fonts-emoji
+      powerline-fonts
+      helvetica-neue-lt-std
+    ];
+  };
+
+  # Configure random setting of wallpapers
+  systemd.user.services.feh-wp = {
+    description = "Randomly set wallpaper via feh";
+    serviceConfig = {
+      Type             = "oneshot";
+      WorkingDirectory = "${pkgs.wallpapers}/share/wallpapers";
+
+      # Manually shuffle because feh's --randomize option can't be restricted to
+      # just certain file types.
+      ExecStart = "${pkgs.bash}/bin/bash -c '${pkgs.fd}/bin/fd -atf | shuf | head -n1 | ${pkgs.findutils}/bin/xargs ${pkgs.feh}/bin/feh --bg-fill'";
+    };
+  };
+
+  systemd.user.timers.feh-wp = {
+    description = "Set a random wallpaper every hour";
+    wantedBy    = [ "graphical-session.target" ];
+    partOf      = [ "graphical-session.target" ];
+
+    timerConfig = {
+      OnActiveSec     = "1second";
+      OnUnitActiveSec = "1hour";
+    };
+  };
+}
diff --git a/ops/infra/nixos/dotfiles.nix b/ops/infra/nixos/dotfiles.nix
new file mode 100644
index 0000000000..2e952207d4
--- /dev/null
+++ b/ops/infra/nixos/dotfiles.nix
@@ -0,0 +1,27 @@
+# Bundle configuration files into a derivation.
+# I call this derivation dotfiles despite that not technically being true
+# anymore ...
+
+{ config, pkgs, ...}:
+
+let dotfiles = pkgs.stdenv.mkDerivation {
+  name = "tazjins-dotfiles";
+
+  srcs = [
+    ./dotfiles
+  ];
+
+  installPhase = ''
+    mkdir -p $out
+    cp ./* $out/
+  '';
+};
+in {
+  # /etc/ is a special place in NixOS!
+  # Symlinks that need to be created there must be specified explicitly.
+  environment.etc = {
+    "alacritty.yml".source    = "${dotfiles}/alacritty.yml";
+    "fish/config.fish".source = "${dotfiles}/config.fish";
+    "tmux.conf".source        = "${dotfiles}/tmux.conf";
+  };
+}
diff --git a/ops/infra/nixos/dotfiles/alacritty.yml b/ops/infra/nixos/dotfiles/alacritty.yml
new file mode 100644
index 0000000000..d229fbdde6
--- /dev/null
+++ b/ops/infra/nixos/dotfiles/alacritty.yml
@@ -0,0 +1,203 @@
+# Configuration for Alacritty, the GPU enhanced terminal emulator
+
+# Any items in the `env` entry below will be added as
+# environment variables. Some entries may override variables
+# set by alacritty it self.
+env:
+  TERM: xterm-256color
+
+window:
+  # TODO
+  decorations: full
+
+scrolling:
+  history: 10000
+  multiplier: 3
+  faux_multiplier: 3
+  auto_scroll: true # TODO
+
+# Display tabs using this many cells (changes require restart)
+tabspaces: 4
+
+# When true, bold text is drawn using the bright variant of colors.
+draw_bold_text_with_bright_colors: true
+
+# Font configuration (changes require restart)
+font:
+  # The normal (roman) font face to use.
+  normal:
+    family: Input Mono
+  bold:
+    family: Input Mono
+  italic:
+    family: Input Mono
+
+  # Point size of the font
+  size: 12.0
+
+  # Scale the font size based on the monitor's DPI.
+  scale_with_dpi: false
+
+# Use custom cursor colors. If true, display the cursor in the cursor.foreground
+# and cursor.background colors, otherwise invert the colors of the cursor.
+custom_cursor_colors: false
+
+# Colors (Gruber Darker)
+colors:
+  # Default colors
+  primary:
+    background: '0x181818'
+    foreground: '0xe4e4ef'
+
+  # Colors the cursor will use if `custom_cursor_colors` is true
+  cursor:
+    text: '0x000000'
+    cursor: '0xf5f5f5'
+
+  # Normal colors
+  normal:
+    black:   '0x282828'
+    red:     '0xf43841'
+    green:   '0x73c936'
+    yellow:  '0xffdd33'
+    blue:    '0x96a6c8'
+    magenta: '0x9e95c7'
+    cyan:    '0x1fad83'
+    white:   '0xf5f5f5'
+
+  # Bright colors
+  bright:
+    black:   '0x484848'
+    red:     '0xff4f58'
+    green:   '0x73c936'
+    yellow:  '0xffdd33'
+    blue:    '0x5f627f'
+    magenta: '0x9e95c7'
+    cyan:    '0x1fad83'
+    white:   '0xffffff'
+
+# Background opacity
+# Key bindings
+#
+# Each binding is defined as an object with some properties. Most of the
+# properties are optional. All of the alphabetical keys should have a letter for
+# the `key` value such as `V`. Function keys are probably what you would expect
+# as well (F1, F2, ..). The number keys above the main keyboard are encoded as
+# `Key1`, `Key2`, etc. Keys on the number pad are encoded `Number1`, `Number2`,
+# etc.  These all match the glutin::VirtualKeyCode variants.
+#
+# Possible values for `mods`
+# `Command`, `Super` refer to the super/command/windows key
+# `Control` for the control key
+# `Shift` for the Shift key
+# `Alt` and `Option` refer to alt/option
+#
+# mods may be combined with a `|`. For example, requiring control and shift
+# looks like:
+#
+# mods: Control|Shift
+#
+# The parser is currently quite sensitive to whitespace and capitalization -
+# capitalization must match exactly, and piped items must not have whitespace
+# around them.
+#
+# Either an `action`, `chars`, or `command` field must be present.
+#   `action` must be one of `Paste`, `PasteSelection`, `Copy`, or `Quit`.
+#   `chars` writes the specified string every time that binding is activated.
+#     These should generally be escape sequences, but they can be configured to
+#     send arbitrary strings of bytes.
+#   `command` must be a map containing a `program` string, and `args` array of
+#     strings. For example:
+#     - { ... , command: { program: "alacritty", args: ["-e", "vttest"] } }
+#
+# Want to add a binding (e.g. "PageUp") but are unsure what the X sequence
+# (e.g. "\x1b[5~") is? Open another terminal (like xterm) without tmux,
+# then run `showkey -a` to get the sequence associated to a key combination.
+key_bindings:
+  - { key: V,        mods: Control|Shift,    action: Paste               }
+  - { key: C,        mods: Control|Shift,    action: Copy                }
+  - { key: Q,        mods: Command, action: Quit                         }
+  - { key: W,        mods: Command, action: Quit                         }
+  - { key: Insert,   mods: Shift,   action: PasteSelection               }
+  - { key: Home,                    chars: "\x1bOH",   mode: AppCursor   }
+  - { key: Home,                    chars: "\x1b[H",   mode: ~AppCursor  }
+  - { key: End,                     chars: "\x1bOF",   mode: AppCursor   }
+  - { key: End,                     chars: "\x1b[F",   mode: ~AppCursor  }
+  - { key: PageUp,   mods: Shift,   chars: "\x1b[5;2~"                   }
+  - { key: PageUp,   mods: Control, chars: "\x1b[5;5~"                   }
+  - { key: PageUp,                  chars: "\x1b[5~"                     }
+  - { key: PageDown, mods: Shift,   chars: "\x1b[6;2~"                   }
+  - { key: PageDown, mods: Control, chars: "\x1b[6;5~"                   }
+  - { key: PageDown,                chars: "\x1b[6~"                     }
+  - { key: Left,     mods: Shift,   chars: "\x1b[1;2D"                   }
+  - { key: Left,     mods: Control, chars: "\x1b[1;5D"                   }
+  - { key: Left,     mods: Alt,     chars: "\x1b[1;3D"                   }
+  - { key: Left,                    chars: "\x1b[D",   mode: ~AppCursor  }
+  - { key: Left,                    chars: "\x1bOD",   mode: AppCursor   }
+  - { key: Right,    mods: Shift,   chars: "\x1b[1;2C"                   }
+  - { key: Right,    mods: Control, chars: "\x1b[1;5C"                   }
+  - { key: Right,    mods: Alt,     chars: "\x1b[1;3C"                   }
+  - { key: Right,                   chars: "\x1b[C",   mode: ~AppCursor  }
+  - { key: Right,                   chars: "\x1bOC",   mode: AppCursor   }
+  - { key: Up,       mods: Shift,   chars: "\x1b[1;2A"                   }
+  - { key: Up,       mods: Control, chars: "\x1b[1;5A"                   }
+  - { key: Up,       mods: Alt,     chars: "\x1b[1;3A"                   }
+  - { key: Up,                      chars: "\x1b[A",   mode: ~AppCursor  }
+  - { key: Up,                      chars: "\x1bOA",   mode: AppCursor   }
+  - { key: Down,     mods: Shift,   chars: "\x1b[1;2B"                   }
+  - { key: Down,     mods: Control, chars: "\x1b[1;5B"                   }
+  - { key: Down,     mods: Alt,     chars: "\x1b[1;3B"                   }
+  - { key: Down,                    chars: "\x1b[B",   mode: ~AppCursor  }
+  - { key: Down,                    chars: "\x1bOB",   mode: AppCursor   }
+  - { key: Tab,      mods: Shift,   chars: "\x1b[Z"                      }
+  - { key: F1,                      chars: "\x1bOP"                      }
+  - { key: F2,                      chars: "\x1bOQ"                      }
+  - { key: F3,                      chars: "\x1bOR"                      }
+  - { key: F4,                      chars: "\x1bOS"                      }
+  - { key: F5,                      chars: "\x1b[15~"                    }
+  - { key: F6,                      chars: "\x1b[17~"                    }
+  - { key: F7,                      chars: "\x1b[18~"                    }
+  - { key: F8,                      chars: "\x1b[19~"                    }
+  - { key: F9,                      chars: "\x1b[20~"                    }
+  - { key: F10,                     chars: "\x1b[21~"                    }
+  - { key: F11,                     chars: "\x1b[23~"                    }
+  - { key: F12,                     chars: "\x1b[24~"                    }
+  - { key: Back,                    chars: "\x7f"                        }
+  - { key: Back,     mods: Alt,     chars: "\x1b\x7f"                    }
+  - { key: Insert,                  chars: "\x1b[2~"                     }
+  - { key: Delete,                  chars: "\x1b[3~"                     }
+
+# Mouse bindings
+#
+# Currently doesn't support modifiers. Both the `mouse` and `action` fields must
+# be specified.
+#
+# Values for `mouse`:
+# - Middle
+# - Left
+# - Right
+# - Numeric identifier such as `5`
+#
+# Values for `action`:
+# - Paste
+# - PasteSelection
+# - Copy (TODO)
+mouse_bindings:
+  - { mouse: Middle, action: PasteSelection }
+
+mouse:
+  double_click: { threshold: 300 }
+  triple_click: { threshold: 300 }
+
+selection:
+  semantic_escape_chars: ",│`|:\"' ()[]{}<>"
+background_opacity: 1.0
+
+hide_cursor_when_typing: false
+
+# Live config reload (changes require restart)
+live_config_reload: true
+
+# Disable visual bell
+visual_bell:
+  duration: 0
diff --git a/ops/infra/nixos/dotfiles/config.fish b/ops/infra/nixos/dotfiles/config.fish
new file mode 100644
index 0000000000..de2c99ae60
--- /dev/null
+++ b/ops/infra/nixos/dotfiles/config.fish
@@ -0,0 +1,40 @@
+# Configure classic prompt
+set fish_color_user --bold blue
+set fish_color_cwd --bold white
+
+# Enable colour hints in VCS prompt:
+set __fish_git_prompt_showcolorhints yes
+set __fish_git_prompt_color_prefix purple
+set __fish_git_prompt_color_suffix purple
+
+# Fish configuration
+set fish_greeting ""
+set PATH $HOME/.local/bin $HOME/.cargo/bin $PATH
+
+# Editor configuration
+set -gx EDITOR "emacsclient"
+set -gx ALTERNATE_EDITOR "emacs -q -nw"
+set -gx VISUAL "emacsclient"
+
+# Miscellaneous
+eval (direnv hook fish)
+
+# Useful command aliases
+alias gpr 'git pull --rebase'
+alias gco 'git checkout'
+alias gf 'git fetch'
+alias gap 'git add -p'
+alias pbcopy 'xclip -selection clipboard'
+alias edit 'emacsclient -n'
+alias servedir 'nix-shell -p haskellPackages.wai-app-static --run warp'
+
+# Old habits die hard (also ls is just easier to type):
+alias ls 'exa'
+
+# Fix up nix-env & friends for Nix 2.0
+export NIX_REMOTE=daemon
+
+# Fix display of fish in emacs' term-mode:
+function fish_title
+  true
+end
diff --git a/ops/infra/nixos/dotfiles/msmtprc b/ops/infra/nixos/dotfiles/msmtprc
new file mode 100644
index 0000000000..624b6a77fc
--- /dev/null
+++ b/ops/infra/nixos/dotfiles/msmtprc
@@ -0,0 +1,16 @@
+defaults
+
+port 587
+tls on
+tls_trust_file /etc/ssl/certs/ca-certificates.crt
+
+# Runbox mail
+account runbox
+from mail@tazj.in
+host mail.runbox.com
+auth on
+user mail@tazj.in
+passwordeval pass show general/runbox-tazjin
+
+# Use Runbox as default
+account default : runbox
diff --git a/ops/infra/nixos/dotfiles/notmuch-config b/ops/infra/nixos/dotfiles/notmuch-config
new file mode 100644
index 0000000000..a490774e63
--- /dev/null
+++ b/ops/infra/nixos/dotfiles/notmuch-config
@@ -0,0 +1,21 @@
+# .notmuch-config - Configuration file for the notmuch mail system
+#
+# For more information about notmuch, see https://notmuchmail.org
+
+[database]
+path=/home/vincent/mail
+
+[user]
+name=Vincent Ambo
+primary_email=mail@tazj.in
+other_email=tazjin@gmail.com;
+
+[new]
+tags=unread;inbox;
+ignore=
+
+[search]
+exclude_tags=deleted;spam;draft;
+
+[maildir]
+synchronize_flags=true
diff --git a/ops/infra/nixos/dotfiles/offlineimaprc b/ops/infra/nixos/dotfiles/offlineimaprc
new file mode 100644
index 0000000000..78315447e4
--- /dev/null
+++ b/ops/infra/nixos/dotfiles/offlineimaprc
@@ -0,0 +1,39 @@
+[general]
+accounts = tazjin, gmail
+
+[DEFAULT]
+ssl = yes
+sslcacertfile = /etc/ssl/certs/ca-certificates.crt
+
+# Private GMail account (old):
+[Account gmail]
+maxage = 90
+localrepository = gmail-local
+remoterepository = gmail-remote
+synclabels = yes
+
+[Repository gmail-local]
+type = GmailMaildir
+localfolders = ~/mail/gmail
+
+[Repository gmail-remote]
+type = Gmail
+remoteuser = tazjin@gmail.com
+remotepassfile = ~/.config/mail/gmail-pass
+folderfilter = lambda folder: folder == 'INBOX'
+
+# Main private account:
+[Account tazjin]
+localrepository = tazjin-local
+remoterepository = tazjin-remote
+
+[Repository tazjin-local]
+type = Maildir
+localfolders = ~/mail/tazjin
+
+[Repository tazjin-remote]
+type = IMAP
+remotehost = mail.runbox.com
+remoteuser = mail@tazj.in
+remotepassfile = ~/.config/mail/tazjin-pass
+auth_mechanisms = LOGIN
diff --git a/ops/infra/nixos/dotfiles/tmux.conf b/ops/infra/nixos/dotfiles/tmux.conf
new file mode 100644
index 0000000000..6c20ae0370
--- /dev/null
+++ b/ops/infra/nixos/dotfiles/tmux.conf
@@ -0,0 +1,14 @@
+set -g status off
+set -gw mode-keys emacs
+setw -g mouse on
+
+# Correctly set window titles
+set -g set-titles on
+set -g set-titles-string "#W (#T)"
+
+# List of plugins
+set -g @plugin 'tmux-plugins/tpm'
+set -g @plugin 'tmux-plugins/tmux-yank'
+
+# Initialize TMUX plugin manager (keep this line at the very bottom of tmux.conf)
+run '~/.tmux/plugins/tpm/tpm'
diff --git a/ops/infra/nixos/home.nix b/ops/infra/nixos/home.nix
new file mode 100644
index 0000000000..236091cb01
--- /dev/null
+++ b/ops/infra/nixos/home.nix
@@ -0,0 +1,75 @@
+# home-manager configuration used on ChromeOS systems
+
+{ config, pkgs, ... }:
+
+{
+  # Allow non-free software (fonts, IDEA, etc.):
+  nixpkgs.config.allowUnfree = true;
+
+  # Install various useful packages:
+  home.packages = with pkgs; [
+    bat
+    exa
+    gnupg
+    google-cloud-sdk
+    htop
+    pass
+    ripgrep
+    tdesktop
+    transmission
+    tree
+
+    # Fonts to make available in X11 applications:
+    input-fonts
+
+    # Emacs configuration stays in the normal ~/.emacs.d location (for
+    # now), hence this package is not installed via `programs.emacs`.
+    (import ./emacs.nix { inherit pkgs; })
+  ];
+
+  programs.git = {
+    enable = true;
+    userEmail = "mail@tazj.in";
+    userName = "Vincent Ambo";
+  };
+
+  services.gpg-agent = {
+    enable = true;
+    extraConfig = ''
+      pinentry-program ${pkgs.pinentry}/bin/pinentry-gtk-2
+      allow-emacs-pinentry
+    '';
+  };
+
+  # Let Home Manager install and manage itself.
+  programs.home-manager.enable = true;
+  manual.html.enable = true;
+
+  # Shell configuration
+  #
+  # There are some differences between the ChromeOS / NixOS
+  # configurations, so instead of fixing up the dotfile to support
+  # both I opted for keeping the configuration here.
+  programs.fish = {
+    enable = true;
+    interactiveShellInit = ''
+      # Configure classic prompt
+      set fish_color_user --bold blue
+      set fish_color_cwd --bold white
+
+      # Enable colour hints in VCS prompt:
+      set __fish_git_prompt_showcolorhints yes
+      set __fish_git_prompt_color_prefix purple
+      set __fish_git_prompt_color_suffix purple
+
+      # Fish configuration
+      set fish_greeting ""
+
+      # Fix up nix-env & friends for Nix 2.0
+      export NIX_REMOTE=daemon
+    '';
+  };
+
+  # Ensure fonts installed via Nix are picked up.
+  fonts.fontconfig.enableProfileFonts = true;
+}
diff --git a/ops/infra/nixos/mail.nix b/ops/infra/nixos/mail.nix
new file mode 100644
index 0000000000..ba4ebfa060
--- /dev/null
+++ b/ops/infra/nixos/mail.nix
@@ -0,0 +1,77 @@
+# This file configures offlineimap, notmuch and MSMTP.
+#
+# Some manual configuration is required the first time this is
+# applied:
+#
+# 1. Credential setup.
+# 2. Linking of MSMTP config (ln -s /etc/msmtprc ~/.msmtprc)
+# 3. Linking of notmuch config (ln -s /etc/notmuch-config ~/.notmuch-config)
+
+{ config, lib, pkgs, ... }:
+
+let offlineImapConfig = pkgs.writeText "offlineimaprc"
+  (builtins.readFile ./dotfiles/offlineimaprc);
+
+msmtpConfig = pkgs.writeText "msmtprc"
+  (builtins.readFile ./dotfiles/msmtprc);
+
+notmuchConfig = pkgs.writeText "notmuch-config"
+  (builtins.readFile ./dotfiles/notmuch-config);
+
+tagConfig = pkgs.writeText "notmuch-tags" ''
+  # Tag emacs-devel mailing list:
+  -inbox +emacs-devel -- to:emacs-devel@gnu.org OR cc:emacs-devel@gnu.org
+
+  # Tag nix-devel mailing list & discourse:
+  -inbox +nix-devel -- to:nix-devel@googlegroups.com OR from:nixos1@discoursemail.com
+
+  # Tag my own mail (from other devices) as sent:
+  -inbox +sent -- from:mail@tazj.in
+
+  # Drafts are always read, duh.
+  -unread -- tag:draft
+'';
+
+notmuchIndex = pkgs.writeShellScriptBin "notmuch-index" ''
+  echo "Indexing new mails in notmuch"
+
+  # Index new mail
+  ${pkgs.notmuch}/bin/notmuch new
+
+  # Apply tags
+  cat ${tagConfig} | ${pkgs.notmuch}/bin/notmuch tag --batch
+
+  echo "Done indexing new mails"
+'';
+in {
+  # Enable OfflineIMAP timer & service:
+  systemd.user.timers.offlineimap = {
+    description = "OfflineIMAP timer";
+    wantedBy    = [ "timers.target" ];
+
+    timerConfig = {
+      Unit       = "offlineimap.service";
+      OnCalendar = "*:0/2"; # every 2 minutes
+      Persistent = "true"; # persist timer state after reboots
+    };
+  };
+
+  systemd.user.services.offlineimap = {
+    description = "OfflineIMAP service";
+    path = with pkgs; [ pass notmuch ];
+
+    serviceConfig = {
+      Type            = "oneshot";
+      ExecStart       = "${pkgs.offlineimap}/bin/offlineimap -u syslog -o -c ${offlineImapConfig}";
+      ExecStartPost   = "${notmuchIndex}/bin/notmuch-index";
+      TimeoutStartSec = "2min";
+    };
+  };
+
+  # Link configuration files to /etc/ (from where they will be linked
+  # further):
+  environment.etc = {
+    "msmtprc".source = msmtpConfig;
+    "notmuch-config".source = notmuchConfig;
+  };
+}
diff --git a/ops/infra/nixos/packages.nix b/ops/infra/nixos/packages.nix
new file mode 100644
index 0000000000..b90a4a9ee8
--- /dev/null
+++ b/ops/infra/nixos/packages.nix
@@ -0,0 +1,132 @@
+# This file contains configuration for packages to install.
+# It does not contain configuration for software that is already covered
+# by other NixOS options (e.g. emacs)
+
+{ config, pkgs, ... }:
+
+let
+  fetchChannel = { rev, sha256 }: import (fetchTarball {
+    inherit sha256;
+    url = "https://github.com/NixOS/nixpkgs-channels/archive/${rev}.tar.gz";
+  }) { config.allowUnfree = true; };
+
+  # Channels last updated: 2018-10-10
+  #
+  # Instead of relying on Nix channels and ending up with out-of-sync
+  # situations between machines, the commit for the stable Nix channel
+  # is pinned here.
+  stable = fetchChannel {
+    rev    = "d96c7a356383302db4426a0d5a8383af921d964f";
+    sha256 = "0hlhczh3m077rwrhp4smf3zd2sfj38h2c126bycv66m0aff0gycn";
+  };
+
+  # Certain packages from unstable are hand-picked into the package
+  # set.
+  unstable = fetchChannel {
+    rev    = "32bcd72bf28a971c9063a9cdcc32effe49f49331";
+    sha256 = "1f74m18r6xl9s55jbkj9bjhdxg2489kwjam4d96pf9rzq0i1f8li";
+  };
+in {
+  # Configure the Nix package manager
+  nixpkgs = {
+    config.allowUnfree = true;
+    # To use the pinned channel, the original package set is thrown
+    # away in the overrides:
+    config.packageOverrides = oldPkgs: stable // {
+      # Store whole unstable channel in case that other modules need
+      # it (see emacs.nix for example):
+      inherit unstable;
+
+      # Backport Exa from unstable until a fix for the Rust builder is
+      # backported.
+      #
+      # https://github.com/NixOS/nixpkgs/pull/48020
+      exa = unstable.exa;
+
+      wallpapers = import ./pkgs/wallpapers.nix;
+      pulseaudio-ctl = import pkgs/pulseaudio-ctl.nix;
+    };
+  };
+
+  # ... and declare packages to be installed.
+  environment.systemPackages = with pkgs; [
+    # Default nixos.* packages:
+    alacritty
+    binutils-unwrapped
+    chromium
+    curl
+    direnv
+    dnsutils
+    dotnet-sdk
+    evince
+    exa
+    extremetuxracer
+    fd
+    file
+    firefox-unwrapped
+    fish
+    gcc
+    git
+    gnumake
+    gnupg
+    google-cloud-sdk
+    gopass
+    hicolor-icon-theme
+    htop
+    i3lock
+    iftop
+    jq
+    kontemplate
+    kubernetes
+    lispPackages.quicklisp
+    lxappearance-gtk3
+    manpages
+    maven
+    mono
+    mq-cli
+    msmtp
+    ngrok
+    notmuch
+    numix-cursor-theme
+    numix-gtk-theme
+    numix-icon-theme
+    offlineimap
+    openjdk
+    openssl
+    openssl.dev
+    pass
+    pavucontrol
+    pkgconfig
+    pulseaudio-ctl
+    pwgen
+    ripgrep
+    rustup
+    sbcl
+    screen
+    siege
+    spotify
+    stdmanpages
+    systemd.dev
+    tdesktop
+    terraform
+    tig
+    tmux
+    tokei
+    transmission
+    tree
+    units
+    unzip
+    vlc
+    xclip
+    xfce.xfce4-screenshooter
+
+    # Haskell packages:
+    cabal-install
+    ghc
+    hlint
+    stack
+    stack2nix
+    haskellPackages.stylish-haskell
+    haskellPackages.yesod-bin
+  ];
+}
diff --git a/ops/infra/nixos/stallo-configuration.nix b/ops/infra/nixos/stallo-configuration.nix
new file mode 100644
index 0000000000..4af4a5fd4c
--- /dev/null
+++ b/ops/infra/nixos/stallo-configuration.nix
@@ -0,0 +1,39 @@
+# Local configuration for 'stallo' (Home desktop PC)
+{ config, pkgs, ...}:
+
+{
+  boot.initrd.luks.devices.stallo-luks.device = "/dev/disk/by-uuid/b484cf1e-a27b-4785-8bd6-fa85a004b073";
+
+  # Use proprietary nvidia driver
+  services.xserver.videoDrivers = [ "nvidia" ];
+
+  # Enable 32-bit compatibility for Steam:
+  hardware.opengl.driSupport32Bit = true;
+  hardware.pulseaudio.support32Bit = true;
+
+  # Wine for Blizzard stuff
+  environment.systemPackages = with pkgs.unstable; [ wineWowPackages.staging winetricks ];
+
+  networking = {
+    hostName = "stallo";
+    wireless.enable = true;
+    wireless.networks = {
+      # Welcome to roast club!
+
+      "How do I computer fast?" = {
+        psk = "washyourface";
+        # Prefer 5Ghz unless the card is acting up.
+        priority = 10;
+      };
+
+      "How do I computer?" = {
+        psk = "washyourface";
+      };
+    };
+    # IPv6 at home, of course:
+    nameservers = [
+      "2606:4700:4700::1111"
+      "2606:4700:4700::1001"
+    ];
+  };
+}