1
0
Fork 0
mirror of https://github.com/Mic92/sops-nix.git synced 2025-04-09 02:14:08 +00:00

Merge pull request #107 from helsinki-systems/feat/age-support-2

Add age support, second attempt
This commit is contained in:
Jörg Thalheim 2021-09-24 13:15:30 +01:00 committed by GitHub
commit 64235a958b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 1039 additions and 136 deletions

View file

@ -9,8 +9,9 @@ Atomic secret provisioning for NixOS based on [sops](https://github.com/mozilla/
Sops-nix decrypts secrets [sops files](https://github.com/mozilla/sops#2usage)
on the target machine to files specified in the NixOS configuration at
activation time. It also adjusts file permissions/owner/group. It uses either
host ssh keys or GPG keys for decryption. In future we will also support cloud
key management APIs such as AWS KMS, GCP KMS, Azure Key Vault or Hashicorp's vault.
age or GPG keys for decryption, where both types can be derived from ssh host
keys. In future we will also support cloud key management APIs such as AWS
KMS, GCP KMS, Azure Key Vault or Hashicorp's vault.
## Features
@ -30,6 +31,17 @@ key management APIs such as AWS KMS, GCP KMS, Azure Key Vault or Hashicorp's vau
There is a configuration.nix example in the [deployment step](#5-deploy) of our usage example.
## Supported encryption methods
sops-nix supports two basic ways of encryption, gnupg and age. Gnupg is based
on gnupg (duh) and encrypts against gnupg public keys. Private gnupg keys may
be used to decrypt the secrets on the target machine. The tool `ssh-to-pgp` can
be used to derive a gnupg key from a ssh (host) key in RSA format.
The other method is age which is based on [age](https://github.com/FiloSottile/age).
A tool is provided with sops-nix that can convert ssh host or user keys in ed25519
format to age keys.
## Usage example
### 1. Install nix-sops
@ -119,7 +131,10 @@ If you use experimental nix flakes support:
}
```
### 2. Generate a GPG key for yourself
### 2a. Generate a GPG key for yourself
This is only needed when you plan to use the gnupg encryption.
When using age, you can skip to step 2b instead.
First generate yourself [a GPG key](https://docs.github.com/en/github/authenticating-to-github/generating-a-new-gpg-key) or use nix-sops
conversion tool to convert an existing ssh key (we only support RSA keys right now):
@ -175,7 +190,32 @@ uid [ unknown] root <root@localhost>
The fingerprint here is `9F89C5F69A10281A835014B09C3DC61F752087EF`.
### 3. Get a PGP Public key for your machine
### 2a. Generate a SSH and age key for yourself
This is only needed when you plan to use the age encryption.
When using gnupg, you need to go back to step 2a.
sops-nix in age mode requires you to have a `ed25519` key. If you don't already
have one, you can generate one using
```console
$ ssh-keygen -t ed25519
```
Converting the public key to the age format works like this:
```console
$ nix-shell -p ssh-to-age --run "ssh-add -L | ssh-to-age"
```
Ssh public key files may also be piped into the `ssh-to-age` tool.
Finally, you need to convert your private key to the age format:
```console
$ mkdir -p ~/.config/sops
$ nix-shell -p ssh-to-age --run "ssh-to-age -private-key -i ~/.ssh/id_ed25519 > ~/.config/sops/age/keys.txt"
```
### 3a. Get a PGP Public key for your machine
The easiest way to add new hosts is using ssh host keys (requires openssh to be enabled).
Since sops does not natively supports ssh keys yet, nix-sops supports a conversion tool
@ -207,11 +247,20 @@ creation_rules:
If you prefer having a separate GnuPG key, see [Use with GnuPG instead of ssh keys](#use-with-gnupg-instead-of-ssh-keys).
### 3b. Get a age Public key for your machine
The `ssh-to-age` tool is used to convert any ssh public key to the age format.
This way you can convert any key:
```console
$ nix-shell -p ssh-to-age --run 'ssh-keyscan my-server.com | ssh-to-age'
$ nix-shell -p ssh-to-age --run 'cat /etc/ssh/ssh_host_ed25519_key.pub | ssh-to-age'
```
### 4. Create a sops file
To create a sops file you need write a `.sops.yaml` as described above and
import your personal gpg key (and your colleagues) and your servers into your
gpg key chain.
To create a sops file you need write a `.sops.yaml` as described above.
When using gnupg you also need to import your personal gpg key
(and your colleagues) and your servers into your gpg key chain.
sops-nix automates importing gpg keys with a hook for nix-shell allowing public
keys to be shared via version control (i.e. git):
@ -366,6 +415,12 @@ If you derived your server public key from ssh, all you need in your configurati
# sops.defaultSopsFile = "/root/.sops/secrets.yaml";
sops.defaultSopsFile = ./secrets.yaml;
sops.secrets.example-key = {};
# This is using ssh keys in the age format:
sops.age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ];
# This is using an age key that is expected to already be in the filesystem
sops.age.keyFile = "/var/lib/sops-nix/key.txt";
# This will generate a new key if the key specified above does not exist
sops.age.generateKey = true;
}
```
@ -678,9 +733,9 @@ If you uploaded it to `/var/lib/sops` than your sops configuration will look lik
```nix
{
# Make sure that `/var/lib/sops` is owned by root and is not world-readable/writable
sops.gnupgHome = "/var/lib/sops";
sops.gnupg.home = "/var/lib/sops";
# disable import host ssh keys
sops.sshKeyPaths = [];
sops.gnupg.sshKeyPaths = [];
}
```

View file

@ -1,5 +1,5 @@
{ pkgs ? import <nixpkgs> {} }: let
vendorSha256 = "sha256-YFy0eIIwrOvAiA+CJNVqY1AgswnPOzxq+GsA82XrT3M=";
vendorSha256 = "sha256:0v99117sshxbnb6kd5vglbdq2kbh71c32g9bjgb24hbjkzns6spp";
sops-install-secrets = pkgs.callPackage ./pkgs/sops-install-secrets {
inherit vendorSha256;

View file

@ -17,7 +17,7 @@
let
localPkgs = import ./default.nix { pkgs = final; };
in {
inherit (localPkgs) sops-install-secrets sops-init-gpg-key sops-pgp-hook sops-import-keys-hook;
inherit (localPkgs) sops-install-secrets sops-init-gpg-key sops-pgp-hook sops-import-keys-hook sops-ssh-to-age;
# backward compatibility
inherit (prev) ssh-to-pgp;
};

40
go.mod
View file

@ -3,11 +3,45 @@ module github.com/Mic92/sops-nix
go 1.14
require (
cloud.google.com/go v0.94.0 // indirect
github.com/Azure/azure-sdk-for-go v57.0.0+incompatible // indirect
github.com/Azure/go-autorest/autorest v0.11.20 // indirect
github.com/Azure/go-autorest/autorest/adal v0.9.15 // indirect
github.com/Azure/go-autorest/autorest/azure/auth v0.5.8 // indirect
github.com/Azure/go-autorest/autorest/azure/cli v0.4.3 // indirect
github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect
github.com/Azure/go-autorest/autorest/validation v0.3.1 // indirect
github.com/Mic92/ssh-to-age v0.0.0-20210829164312-1fe15380abe4
github.com/ProtonMail/go-crypto v0.0.0-20210707164159-52430bf6b52c
github.com/mozilla-services/yaml v0.0.0-20191106225358-5c216288813c
github.com/aws/aws-sdk-go v1.40.34 // indirect
github.com/cenkalti/backoff/v3 v3.2.2 // indirect
github.com/fatih/color v1.12.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-retryablehttp v0.7.0 // indirect
github.com/hashicorp/vault/api v1.1.1 // indirect
github.com/howeyc/gopass v0.0.0-20190910152052-7cb4b85ec19c // indirect
github.com/lib/pq v1.10.2 // indirect
github.com/mattn/go-isatty v0.0.13 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
github.com/mitchellh/mapstructure v1.4.1 // indirect
github.com/mozilla-services/yaml v0.0.0-20201007153854-c369669a6625
github.com/pierrec/lz4 v2.6.1+incompatible // indirect
github.com/sirupsen/logrus v1.8.1 // indirect
go.mozilla.org/sops/v3 v3.7.1
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2
golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5
golang.org/x/net v0.0.0-20210825183410-e898025ed96a // indirect
golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect
google.golang.org/api v0.56.0 // indirect
gopkg.in/ini.v1 v1.62.0 // indirect
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
)
// see https://github.com/mozilla/sops/pull/925

705
go.sum

File diff suppressed because it is too large Load diff

View file

@ -85,7 +85,10 @@ let
# Does this need to be configurable?
secretsMountPoint = "/run/secrets.d";
symlinkPath = "/run/secrets";
inherit (cfg) gnupgHome sshKeyPaths;
gnupgHome = cfg.gnupg.home;
sshKeyPaths = cfg.gnupg.sshKeyPaths;
ageKeyFile = cfg.age.keyFile;
ageSshKeyPaths = cfg.age.sshKeyPaths;
});
checkedManifest = let
@ -130,30 +133,67 @@ in {
'';
};
gnupgHome = mkOption {
type = types.nullOr types.str;
default = null;
example = "/root/.gnupg";
description = ''
Path to gnupg database directory containing the key for decrypting sops file.
'';
age = {
keyFile = mkOption {
type = types.nullOr types.path;
default = null;
example = "/var/lib/sops-nix/key.txt";
description = ''
Path to age key file used for sops decryption.
Setting this to a non-null value causes the ssh keys to be ignored.
'';
};
generateKey = mkOption {
type = types.bool;
default = false;
description = ''
Whether or not to generate the age key. If this
option is set to false, the key must already be
present at the specified location.
'';
};
sshKeyPaths = mkOption {
type = types.listOf types.path;
default = if config.services.openssh.enable then map (e: e.path) (lib.filter (e: e.type == "ed25519") config.services.openssh.hostKeys) else [];
description = ''
Paths to ssh keys added as age keys during sops description.
This setting is ignored when the keyFile is set to a non-null value.
'';
};
};
sshKeyPaths = mkOption {
type = types.listOf types.path;
default = if config.services.openssh.enable then
map (e: e.path) (lib.filter (e: e.type == "rsa") config.services.openssh.hostKeys)
else [];
description = ''
Path to ssh keys added as GPG keys during sops description.
This option must be explicitly unset if <literal>config.sops.sshKeyPaths</literal>.
'';
gnupg = {
home = mkOption {
type = types.nullOr types.str;
default = null;
example = "/root/.gnupg";
description = ''
Path to gnupg database directory containing the key for decrypting the sops file.
'';
};
sshKeyPaths = mkOption {
type = types.listOf types.path;
default = if config.services.openssh.enable then
map (e: e.path) (lib.filter (e: e.type == "rsa") config.services.openssh.hostKeys)
else [];
description = ''
Path to ssh keys added as GPG keys during sops description.
This option must be explicitly unset if <literal>config.sops.gnupg.sshKeyPaths</literal> is set.
'';
};
};
};
imports = [
(mkRenamedOptionModule [ "sops" "gnupgHome" ] [ "sops" "gnupg" "home" ])
(mkRenamedOptionModule [ "sops" "sshKeyPaths" ] [ "sops" "gnupg" "sshKeyPaths" ])
];
config = mkIf (cfg.secrets != {}) {
assertions = [{
assertion = (cfg.gnupgHome == null) != (cfg.sshKeyPaths == []);
message = "Exactly one of sops.gnupgHome and sops.sshKeyPaths must be set";
assertion = (cfg.age.keyFile == null && cfg.age.sshKeyPaths == []) -> (cfg.gnupg.home == null) != (cfg.gnupg.sshKeyPaths == []);
message = "Exactly one of sops.gnupg.home and sops.gnupg.sshKeyPaths must be set for gnupg mode";
}] ++ optionals cfg.validateSopsFiles (
concatLists (mapAttrsToList (name: secret: [{
assertion = builtins.pathExists secret.sopsFile;
@ -168,9 +208,18 @@ in {
system.activationScripts.setup-secrets = let
sops-install-secrets = (pkgs.callPackage ../.. {}).sops-install-secrets;
in stringAfter [ "specialfs" "users" "groups" ] ''
in stringAfter ([ "specialfs" "users" "groups" ] ++ optional cfg.age.generateKey "generate-age-key") ''
echo setting up secrets...
${optionalString (cfg.gnupgHome != null) "SOPS_GPG_EXEC=${pkgs.gnupg}/bin/gpg"} ${sops-install-secrets}/bin/sops-install-secrets ${checkedManifest}
${optionalString (cfg.gnupg.home != null) "SOPS_GPG_EXEC=${pkgs.gnupg}/bin/gpg"} ${sops-install-secrets}/bin/sops-install-secrets ${checkedManifest}
'';
system.activationScripts.generate-age-key = (mkIf cfg.age.generateKey) (stringAfter [] ''
if [[ ! -f "${cfg.age.keyFile}" ]]; then;
echo generating machine-specific age key...
mkdir -p $(dirname ${cfg.age.keyFile})
# age-keygen sets 0600 by default, no need to chmod.
${pkgs.age}/bin/age-keygen -o ${cfg.age.keyFile}
fi
'');
};
}

View file

@ -5,7 +5,6 @@ package main
import (
"encoding/hex"
"encoding/json"
"errors"
"flag"
"fmt"
"io/ioutil"
@ -16,8 +15,10 @@ import (
"strconv"
"strings"
"syscall"
"time"
"github.com/Mic92/sops-nix/pkgs/sops-install-secrets/sshkeys"
agessh "github.com/Mic92/ssh-to-age"
"github.com/mozilla-services/yaml"
"go.mozilla.org/sops/v3/decrypt"
@ -47,6 +48,8 @@ type manifest struct {
SymlinkPath string `json:"symlinkPath"`
SSHKeyPaths []string `json:"sshKeyPaths"`
GnupgHome string `json:"gnupgHome"`
AgeKeyFile string `json:"ageKeyFile"`
AgeSshKeyPaths []string `json:"ageSshKeyPaths"`
}
type secretFile struct {
@ -437,10 +440,17 @@ func (app *appContext) validateManifest() error {
if m.SymlinkPath == "" {
m.SymlinkPath = "/run/secrets"
}
if len(m.SSHKeyPaths) > 0 && m.GnupgHome != "" {
return errors.New("gnupgHome and sshKeyPaths were specified in the manifest. " +
"Both options are mutual exclusive.")
if m.GnupgHome != "" {
errorFmt := "gnupgHome and %s were specified in the manifest. " +
"Both options are mutually exclusive."
if len(m.SSHKeyPaths) > 0 {
return fmt.Errorf(errorFmt, "sshKeyPaths")
}
if m.AgeKeyFile != "" {
return fmt.Errorf(errorFmt, "ageKeyFile")
}
}
for i := range m.Secrets {
if err := app.validateSecret(&m.Secrets[i]); err != nil {
return err
@ -509,6 +519,35 @@ func importSSHKeys(keyPaths []string, gpgHome string) error {
return nil
}
func importAgeSSHKeys(keyPaths []string, ageFilePath string) error {
ageFile, err := os.OpenFile(ageFilePath, os.O_WRONLY|os.O_CREATE, 0600)
if err != nil {
return fmt.Errorf("Cannot create '%s': %w", ageFilePath, err)
}
defer ageFile.Close()
fmt.Fprintf(ageFile, "# generated by sops-nix at %s\n", time.Now().Format(time.RFC3339))
for _, p := range keyPaths {
// Read the key
sshKey, err := ioutil.ReadFile(p)
if err != nil {
return fmt.Errorf("Cannot read ssh key '%s': %w", p, err)
}
// Convert the key to age
bech32, err := agessh.SSHPrivateKeyToAge(sshKey)
if err != nil {
return fmt.Errorf("Cannot convert ssh key '%s': %w", p, err)
}
// Append it to the file
_, err = ageFile.WriteString(*bech32 + "\n")
if err != nil {
return fmt.Errorf("Cannot write key to age file: %w", err)
}
}
return nil
}
type keyring struct {
path string
}
@ -604,6 +643,15 @@ func installSecrets(args []string) error {
defer keyring.Remove()
} else if manifest.GnupgHome != "" {
os.Setenv("GNUPGHOME", manifest.GnupgHome)
} else if len(manifest.AgeSshKeyPaths) != 0 {
keyfile := filepath.Join(manifest.SecretsMountPoint, "age-keys.txt")
err = importAgeSSHKeys(manifest.AgeSshKeyPaths, keyfile)
if err != nil {
return err
}
os.Setenv("SOPS_AGE_KEY_FILE", keyfile)
} else if manifest.AgeKeyFile != "" {
os.Setenv("SOPS_AGE_KEY_FILE", manifest.AgeKeyFile)
}
if err := decryptSecrets(manifest.Secrets); err != nil {

View file

@ -219,6 +219,72 @@ func testSSHKey(t *testing.T) {
testInstallSecret(t, testdir, &m)
}
func TestAge(t *testing.T) {
assets := testAssetPath()
testdir := newTestDir(t)
defer testdir.Remove()
target := path.Join(testdir.path, "existing-target")
file, err := os.Create(target)
ok(t, err)
file.Close()
s := secret{
Name: "test",
Key: "test_key",
Owner: "nobody",
Group: "nogroup",
SopsFile: path.Join(assets, "secrets.yaml"),
Path: target,
Mode: "0400",
RestartServices: []string{"affected-service"},
ReloadServices: make([]string, 0),
}
m := manifest{
Secrets: []secret{s},
SecretsMountPoint: testdir.secretsPath,
SymlinkPath: testdir.symlinkPath,
AgeKeyFile: path.Join(assets, "age-keys.txt"),
}
testInstallSecret(t, testdir, &m)
}
func TestAgeWithSSH(t *testing.T) {
assets := testAssetPath()
testdir := newTestDir(t)
defer testdir.Remove()
target := path.Join(testdir.path, "existing-target")
file, err := os.Create(target)
ok(t, err)
file.Close()
s := secret{
Name: "test",
Key: "test_key",
Owner: "nobody",
Group: "nogroup",
SopsFile: path.Join(assets, "secrets.yaml"),
Path: target,
Mode: "0400",
RestartServices: []string{"affected-service"},
ReloadServices: make([]string, 0),
}
m := manifest{
Secrets: []secret{s},
SecretsMountPoint: testdir.secretsPath,
SymlinkPath: testdir.symlinkPath,
AgeSshKeyPaths: []string{path.Join(assets, "ssh-ed25519-key")},
}
testInstallSecret(t, testdir, &m)
}
func TestAll(t *testing.T) {
// we can't test in parallel because we rely on GNUPGHOME environment variable
testGPG(t)

View file

@ -23,6 +23,50 @@
inherit (pkgs) system;
};
age-keys = makeTest {
name = "sops-age-keys";
machine = {
imports = [ ../../modules/sops ];
sops = {
age.keyFile = ./test-assets/age-keys.txt;
defaultSopsFile = ./test-assets/secrets.yaml;
secrets.test_key = {};
};
};
testScript = ''
start_all()
machine.succeed("cat /run/secrets/test_key | grep -q test_value")
'';
} {
inherit pkgs;
inherit (pkgs) system;
};
age-ssh-keys = makeTest {
name = "sops-age-ssh-keys";
machine = {
imports = [ ../../modules/sops ];
services.openssh.enable = true;
services.openssh.hostKeys = [{
type = "ed25519";
path = ./test-assets/ssh-ed25519-key;
}];
sops = {
defaultSopsFile = ./test-assets/secrets.yaml;
secrets.test_key = {};
};
};
testScript = ''
start_all()
machine.succeed("cat /run/secrets/test_key | grep -q test_value")
'';
} {
inherit pkgs;
inherit (pkgs) system;
};
pgp-keys = makeTest {
name = "sops-pgp-keys";
nodes.server = { pkgs, lib, config, ... }: {
@ -35,7 +79,7 @@
group = "nogroup";
};
sops.gnupgHome = "/run/gpghome";
sops.gnupg.home = "/run/gpghome";
sops.defaultSopsFile = ./test-assets/secrets.yaml;
sops.secrets.test_key.owner = config.users.users.someuser.name;
sops.secrets.existing-file = {

View file

@ -0,0 +1,3 @@
# created: 2020-07-18T03:16:47-07:00
# public key: age1yt3tfqlfrwdwx0z0ynwplcr6qxcxfaqycuprpmy89nr83ltx74tqdpszlw
AGE-SECRET-KEY-1NJT5YCS2LWU4V4QAJQ6R4JNU7LXPDX602DZ9NUFANVU5GDTGUWCQ5T59M6

View file

@ -1,47 +1,66 @@
test_key: ENC[AES256_GCM,data:4cC2PTi7xVPZPA==,iv:voX4IQemcgt0O97oLExy5r2V85nn687cIyWmHNDhUag=,tag:ZaKi9m6ziFKNV+gx7XedTw==,type:str]
test_key: ENC[AES256_GCM,data:2mP+IAdczoEr0g==,iv:voX4IQemcgt0O97oLExy5r2V85nn687cIyWmHNDhUag=,tag:R97qy4fKneU7D9UFhXNvgA==,type:str]
a_list:
- ENC[AES256_GCM,data:5K0=,iv:5P+1UQyIYOW8xXgsvTXC17msGcA6IGB3N8n+pstfqjo=,tag:Op0+iEYzV+gfYGveN3VKKg==,type:str]
- ENC[AES256_GCM,data:9dM=,iv:LbGS8DjM6Vnr2nU7QokzQlg0gL+XMWhqbN+ypP7ZIZo=,tag:HvbERoLZcUOjEd4AwLVNEg==,type:str]
- ENC[AES256_GCM,data:oOQ=,iv:5P+1UQyIYOW8xXgsvTXC17msGcA6IGB3N8n+pstfqjo=,tag:ox4rgjbb8c0vYZ2XmwRgpg==,type:str]
- ENC[AES256_GCM,data:mYU=,iv:LbGS8DjM6Vnr2nU7QokzQlg0gL+XMWhqbN+ypP7ZIZo=,tag:CFrhnZv6lYGJOVso+2YBFg==,type:str]
sops:
kms: []
gcp_kms: []
azure_kv: []
hc_vault: []
lastmodified: '2021-01-27T06:12:22Z'
mac: ENC[AES256_GCM,data:/lwT78drEKdCoWW9TPU2H/IWlq/9uEmJocrTvftKTD1Au9e/7AMCUWGWMPGJKMg9R0FWV2pn3tgwli5YXRrIe4L9tIkeM5vJvz85IeQIc+vviby7PM8VtbO1ArisHh95cVwZuASR3KSbumnxURjayZ61J9Jiz0viBeuEmCP50u4=,iv:FX6XDUqetDaRTtLLfMaJAkPZmiZx59wnuDRm0SvmTJM=,tag:HnQ4dHsCCNim2v8WPXyLdw==,type:str]
age:
- recipient: age1yt3tfqlfrwdwx0z0ynwplcr6qxcxfaqycuprpmy89nr83ltx74tqdpszlw
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBOcUlPTGtUY2R1Si9SUnpN
dGVMVzMzTXd2Z09UeGdxQlNqeVg3WTFadWtNCkpzaVJqMkZ0b1JUamJsbysrdGll
QVlLNU9xMnZqandMdGxKNlZ2amJFbncKLS0tIFlrbGtyMkZKNUthd0Z5VW5MVjBN
bWFhWGlJaXMzWUJoZGpnMjNoSnlMYTgK2hM/Cc6xN1xkluL69jDaaoaEijAJk+l8
TwhUG7Qlggod2xCWTC4cpjb+THip2u31tFoSPQZKEG8gDcGNIz2HOw==
-----END AGE ENCRYPTED FILE-----
- recipient: age1a8pk4akrdamj7nvqy3zywgtny8dxz7t5xzu7u8v9mhrayp9freqsqatyrs
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBCS2NvekFqa2dVeXUzRzZY
emR6NDJqMnptOHpQUDBjTjdaMGp4SGZ1bTJVCk16Tnd3ZFl0dFUzQ1RNRzYyclV3
d24xeDZMaXFvbEhZWXRObTM4RWRQMk0KLS0tIGs4bjFweEFjblFEalBOLzMyYyt6
WHJmNkhFVStxRG5PZTZUWnRFTmtzemsKLXKJN3GSJKDI4MYPxDU5HbTzoSAt0jK9
T9sJbd++By2OC9rl+GJoJcy4aM0uTYy83EDfqBV02Y1CfepRjHLRWQ==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2021-08-27T17:58:39Z"
mac: ENC[AES256_GCM,data:V9QGBTrofAza2LK1hA5cQmuT37BsfRJZZtSmvc5CDnIeWYTLUOkCFRzY+wk3uZNj0aM3BUAjvIM6LPNJ+rR8p0vXwKN37UH/oRmGADuLcu5Ec7hmArCGjKMo0gyaarlvuGmFhjb1gcW2PAbo84lDykxbTyf7Pp7APJhIvGbwBu4=,iv:ufG8sG5NAtVO25kZXrWnSQ/kkbDlwmOWhO4T9UpRvOg=,tag:YidkKTBqH48N7bjBabrAgQ==,type:str]
pgp:
- created_at: '2020-07-12T08:03:51Z'
enc: |
- created_at: "2020-07-12T08:03:51Z"
enc: |
-----BEGIN PGP MESSAGE-----
hQEMA/m6nevQP1fAAQgAnGLEGUnWgYTLGGZN7sEETu14MJjr4TY7JDhs8VOrA9ng
x9ivSF1dPw6arewal70OgxMLnS55HY6L81vRCbu8d0rdfXkdzO6oeMAo79/udKc8
F5CDSkdrBImK5xpEQ0stpt1zmmKMAPGuPSbSc48YEt4OYUdpoIO+PesYUUgGerL7
4UvXtbhYjIhZL7Sgst/coOUNUWjpQgVRSeza6qcuVfS3gVBvxNCed51n+Cr3/rnD
5PXjeWQ8aayLDDV32CjGf+s3+LG0gvJ7A6eq0THjsvgkY4qGHNSMobkN7npFI4lX
KZG68Scu5d11IY2N3y0ijRQQxYlPPPpukWvw3CK8htJeAZltLDHzh0bpISpc7gqU
xzATRG/LmJk7yS9Vej/B0NGmkVlbB+lFnLZG3lVmDiaQUsgePp2Ho1/j6ysit3/C
i87IM8B71y1brldk2mcr1oAoNSVE4dg+/C2HrDRKxA==
=Qyuq
hQEMA/m6nevQP1fAAQgApvcEy9FBr6kag0PkFBabiEhqtKG6CcN/ewbxfDGXbOPI
hyndS7Poc6a7VeYo6cDQwxNqbUbjjn6BRZBFGHxuVInjvtDVm2phh/HOd25IH68s
RGh93wyW637rqJGp8+X3of7b+XBxq0fg0hLqKxR8iMaVF3WnyAMfS/r1tAOuHRGF
geMSQftnWkv1OIl2DPDcv02lqHSKqVZpidzxEdeAqAH/Ml9SoTOEyC8uNz0LIdvP
SQUp5JFp5CEyXaAzeTiypodIjCKOmCNTLuR9VC8O5+P+E62xVmxoFVVfozg2ZBdk
CJrEGR5jxTxAI1IB+ywWOde+cVzQtPXds1d3at2uFtJeAZuS3VYfvL1f4rXNrSBV
3x7+rDknN8PsFAmmnLdxtbPJAij9eERpoAOsJOy6Ka4OSvOj4sCCU09wb2i/PugU
a7y4M55KzV/8J5aQ4iMVym/9Gkb98XK2Ff5na1jQGQ==
=MU7I
-----END PGP MESSAGE-----
fp: 7FB89715AADA920D65D25E63F9BA9DEBD03F57C0
- created_at: '2020-07-12T08:03:51Z'
enc: |
fp: 7FB89715AADA920D65D25E63F9BA9DEBD03F57C0
- created_at: "2020-07-12T08:03:51Z"
enc: |
-----BEGIN PGP MESSAGE-----
hQGMA3ulPRkZxd/UAQv/d1ExCl7gVIe0GeH8bc0xl8gt4hvR2ujH0BmmHpQ7SDLa
tS7lH3ennlLlN+owN3DkCKqm/BNEQ7Wy5y0oTvmQSo2BdSMYOvHrLKoPK3OsDLPH
vtXUKZ+NLKSV2xCSiqrcu4GRki8klLgDNEgna8T21erCXZM8W/ehcOd7gLSUiatd
wQTGEhNAA0RO04kf0NoPcNrPak7e0bGbwEc88RdM2QyxSXi6SVrKVHvYVPE3pfFB
VkUcWZU2edLUEKIAFRAtmb9s3DSjECMMuoVXR6lrPx4c6OMRKvxtlZh0uOOcRpoG
M5KL8O4ZXNxUVNRzPisgqe0hkh2AsnMLIQeWrVLfOLXUdDjr9x42vga/P8WU7AzT
/Lxmlu9cY0KKsXpv7yXibZfFJ/K/4Mo9WsNk6tGAmiSUg1hr/NJPha3PDHyX0ER7
6dPFeMrvs4VngC0I27v6rlEjU+HiRv3/uLnn04z9DRQEk6a+YpLZTpHvAbzfb1Dg
EBpKCQePm2rIzywLE8d60lABXSQKG4/LSH6WgxfRFibORME2z5DD2G7ibiDbVg8j
YbrH0E0KeRxYJMJ28FVDlLgoUL+JbNr70uHkdvz8S0hqCYwo+K4KqUOIsiPc6iIL
tA==
=XoKf
hQGMA3ulPRkZxd/UAQwAidXXZa5HVHCuI9pULCMVfX25pjYk3CpGdo1jLt20teRu
QVe5Ner7Z3QF8BMk4YRDDaJWlWLbHQE4KYM5/ER/iJyrSIp9wcIx7bQvoCO44KLh
5wXbmxRnscUaW67+qdnjZBFSIHxtaeRYSBGCk3CGODnVamvGXdv733eG/O2IBHqt
sIE3+cOk6N+gQxYcz5IxJlRJlF6NagD4RxdMzjx6QJ43pZp8tKupDFZ1Teh1c4mY
8XtVekaWz9ToKiQD3uQoCIwSW/YszuviYf/ar4Bi7j2xTH9vzSxxoRsSjo0JXKyB
EDj2Y1M5KzZAb3OWNINmNt2jqwKF8HS06TrbP6bdmRgHWRnwJLaSHSpxiclT/YpC
En4/ZvjqJdxyJc0nmEyDpEgelpTzm19jzFvsEvj43GnnWjh6/aAb0TF2Ms1E7I5E
VpJFI7l/I1JDacdDlvx1jFMhsya9n356GhZaiJky89hURsHhH5ek8E3f0PpC20dp
J7o8e7N0zXV39iIw6kdT0lABPe8KVRzQOsIKqNGaVwZVQuX4i/C1vbz6yTb+cHc6
yCYEoi674QYg7ofZV4VkY318XOQz7P5sVlASjADvKF9SzjENadp8Y0SHvuYkXU4W
yQ==
=DqFU
-----END PGP MESSAGE-----
fp: 2504791468B153B8A3963CC97BA53D1919C5DFD4
fp: 2504791468B153B8A3963CC97BA53D1919C5DFD4
unencrypted_suffix: _unencrypted
version: 3.6.1

View file

@ -0,0 +1,7 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACASNfkPp7cwDXm7Z4nAogGNvbqixljmjhixGvG1KjlZkgAAAJjHY9ZUx2PW
VAAAAAtzc2gtZWQyNTUxOQAAACASNfkPp7cwDXm7Z4nAogGNvbqixljmjhixGvG1KjlZkg
AAAEC5eNs176OO7IO8ap33TVXlOxhhQYcYtv3VW+/5Ft8UohI1+Q+ntzANebtnicCiAY29
uqLGWOaOGLEa8bUqOVmSAAAAEHNhcmF0QHNhcmF0LWRlbGwBAgMEBQ==
-----END OPENSSH PRIVATE KEY-----

View file

@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBI1+Q+ntzANebtnicCiAY29uqLGWOaOGLEa8bUqOVmS