clan flakes create: initialize keys automatically (#4435)
fixes https://git.clan.lol/clan/clan-core/issues/2665 fixes https://git.clan.lol/clan/clan-core/issues/4407 Co-authored-by: DavHau <d.hauer.it@gmail.com> Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4435 Co-authored-by: Jörg Thalheim <joerg@thalheim.io> Co-committed-by: Jörg Thalheim <joerg@thalheim.io>
This commit is contained in:
@@ -24,7 +24,7 @@ If you're new to Clan and eager to dive in, start with our quickstart guide and
|
||||
|
||||
In the Clan ecosystem, security is paramount. Learn how to handle secrets effectively:
|
||||
|
||||
- **Secrets Management**: Securely manage secrets by consulting [secrets](https://docs.clan.lol/guides/getting-started/secrets/)<!-- [secrets.md](docs/site/guides/getting-started/secrets.md) -->.
|
||||
- **Secrets Management**: Securely manage secrets by consulting [Vars](https://docs.clan.lol/guides/vars-backend/)<!-- [secrets.md](docs/site/guides/vars-backend.md) -->.
|
||||
|
||||
### Contributing to Clan
|
||||
|
||||
|
||||
@@ -53,15 +53,15 @@ nav:
|
||||
- ⚙️ Add Machines: guides/getting-started/add-machines.md
|
||||
- ⚙️ Add User: guides/getting-started/add-user.md
|
||||
- ⚙️ Add Services: guides/getting-started/add-services.md
|
||||
- 🔐 Secrets & Facts: guides/getting-started/secrets.md
|
||||
- 🚢 Deploy Machine: guides/getting-started/deploy.md
|
||||
- 🧪 Continuous Integration: guides/getting-started/check.md
|
||||
- clanServices: guides/clanServices.md
|
||||
- Disk Encryption: guides/disk-encryption.md
|
||||
- Mesh VPN: guides/mesh-vpn.md
|
||||
- Backup & Restore: guides/backups.md
|
||||
- Vars Backend: guides/vars-backend.md
|
||||
- Facts Backend: guides/secrets.md
|
||||
- Vars: guides/vars-backend.md
|
||||
- Age Plugins: guides/age-plugins.md
|
||||
- Secrets (Advanced Usage): guides/secrets.md
|
||||
- Adding more machines: guides/more-machines.md
|
||||
- Target Host: guides/target-host.md
|
||||
- Inventory:
|
||||
@@ -246,3 +246,6 @@ plugins:
|
||||
- search
|
||||
- macros
|
||||
- redoc-tag
|
||||
- redirects:
|
||||
redirect_maps:
|
||||
guides/getting-started/secrets.md: guides/vars-backend.md
|
||||
|
||||
@@ -40,6 +40,7 @@ pkgs.stdenv.mkDerivation {
|
||||
mkdocs-material
|
||||
mkdocs-macros
|
||||
mkdocs-redoc-tag
|
||||
mkdocs-redirects
|
||||
]);
|
||||
configurePhase = ''
|
||||
pushd docs
|
||||
|
||||
59
docs/site/guides/age-plugins.md
Normal file
59
docs/site/guides/age-plugins.md
Normal file
@@ -0,0 +1,59 @@
|
||||
## Using Age Plugins
|
||||
|
||||
If you wish to use a key generated using an [age plugin] as your admin key, extra care is needed.
|
||||
|
||||
You must **precede your secret key with a comment that contains its corresponding recipient**.
|
||||
|
||||
This is usually output as part of the generation process
|
||||
and is only required because there is no unified mechanism for recovering a recipient from a plugin secret key.
|
||||
|
||||
Here is an example:
|
||||
|
||||
```title="~/.config/sops/age/keys.txt"
|
||||
# public key: age1zdy49ek6z60q9r34vf5mmzkx6u43pr9haqdh5lqdg7fh5tpwlfwqea356l
|
||||
AGE-PLUGIN-FIDO2-HMAC-1QQPQZRFR7ZZ2WCV...
|
||||
```
|
||||
|
||||
!!! note
|
||||
The comment that precedes the plugin secret key need only contain the recipient.
|
||||
Any other text is ignored.
|
||||
|
||||
In the example above, you can specify `# recipient: age1zdy...`, `# public: age1zdy....` or even
|
||||
just `# age1zdy....`
|
||||
|
||||
You will need to add an entry into your `flake.nix` to ensure that the necessary `age` plugins
|
||||
are loaded when using Clan:
|
||||
|
||||
```nix title="flake.nix"
|
||||
{
|
||||
inputs.clan-core.url = "https://git.clan.lol/clan/clan-core/archive/main.tar.gz";
|
||||
inputs.nixpkgs.follows = "clan-core/nixpkgs";
|
||||
|
||||
outputs =
|
||||
{ self, clan-core, ... }:
|
||||
let
|
||||
# Sometimes this attribute set is defined in clan.nix
|
||||
clan = clan-core.lib.clan {
|
||||
inherit self;
|
||||
|
||||
meta.name = "myclan";
|
||||
|
||||
# Add Yubikey and FIDO2 HMAC plugins
|
||||
# Note: the plugins listed here must be available in nixpkgs.
|
||||
secrets.age.plugins = [
|
||||
"age-plugin-yubikey"
|
||||
"age-plugin-fido2-hmac"
|
||||
];
|
||||
|
||||
machines = {
|
||||
# elided for brevity
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
inherit (clan) nixosConfigurations nixosModules clanInternals;
|
||||
|
||||
# elided for brevity
|
||||
};
|
||||
}
|
||||
```
|
||||
@@ -8,7 +8,6 @@ Now that you have created a machines, added some services and setup secrets. Thi
|
||||
- [x] RAM > 2GB
|
||||
- [x] **Two Computers**: You need one computer that you're getting ready (we'll call this the Target Computer) and another one to set it up from (we'll call this the Setup Computer). Make sure both can talk to each other over the network using SSH.
|
||||
- [x] **Machine configuration**: See our basic [adding and configuring machine guide](./add-machines.md)
|
||||
- [x] **Initialized secrets**: See [secrets](secrets.md) for how to initialize your secrets.
|
||||
|
||||
## Physical Hardware
|
||||
|
||||
|
||||
@@ -130,6 +130,5 @@ You can continue with **any** of the following steps at your own pace:
|
||||
- [ ] [Add Machines](./add-machines.md)
|
||||
- [ ] [Add a User](./add-user.md)
|
||||
- [ ] [Add Services](./add-services.md)
|
||||
- [ ] [Configure Secrets](./secrets.md)
|
||||
- [ ] [Deploy](./deploy.md) - Requires configured secrets
|
||||
- [ ] [Setup CI (optional)](./check.md)
|
||||
|
||||
@@ -1,179 +0,0 @@
|
||||
|
||||
Setting up secrets is **Required** for any *machine deployments* or *vm runs* - You need to complete the steps: [Create Admin Keypair](#create-your-admin-keypair) and [Add Your Public Key(s)](#add-your-public-keys)
|
||||
|
||||
---
|
||||
|
||||
Clan enables encryption of secrets (such as passwords & keys) ensuring security and ease-of-use among users.
|
||||
|
||||
By default, Clan uses the [sops](https://github.com/getsops/sops) format
|
||||
and integrates with [sops-nix](https://github.com/Mic92/sops-nix) on NixOS machines.
|
||||
Clan can also be configured to be used with other secret store [backends](../../reference/clan.core/vars.md#clan.core.vars.settings.secretStore).
|
||||
|
||||
This guide will walk you through:
|
||||
|
||||
- **Creating a Keypair for Your User**: Learn how to generate a keypair for `$USER` to securely control all secrets.
|
||||
- **Creating Your First Secret**: Step-by-step instructions on creating your initial secret.
|
||||
- **Assigning Machine Access to the Secret**: Understand how to grant a machine access to the newly created secret.
|
||||
|
||||
## Create Your Admin Keypair
|
||||
|
||||
To get started, you'll need to create **your admin keypair**.
|
||||
|
||||
!!! info
|
||||
Don't worry — if you've already made one before, this step won't change or overwrite it.
|
||||
|
||||
```bash
|
||||
clan secrets key generate
|
||||
```
|
||||
|
||||
**Output**:
|
||||
|
||||
```{.console, .no-copy}
|
||||
Public key: age1wkth7uhpkl555g40t8hjsysr20drq286netu8zptw50lmqz7j95sw2t3l7
|
||||
|
||||
Generated age private key at '/home/joerg/.config/sops/age/keys.txt' for your user. Please back it up on a secure location or you will lose access to your secrets.
|
||||
Also add your age public key to the repository with 'clan secrets users add YOUR_USER age1wkth7uhpkl555g40t8hjsysr20drq286netu8zptw50lmqz7j95sw2t3l7' (replace YOUR_USER with your actual username)
|
||||
```
|
||||
|
||||
!!! warning
|
||||
Make sure to keep a safe backup of the private key you've just created.
|
||||
If it's lost, you won't be able to get to your secrets anymore because they all need the admin key to be unlocked.
|
||||
|
||||
If you already have an [age] secret key and want to use that instead, you can simply edit `~/.config/sops/age/keys.txt`:
|
||||
|
||||
```title="~/.config/sops/age/keys.txt"
|
||||
AGE-SECRET-KEY-13GWMK0KNNKXPTJ8KQ9LPSQZU7G3KU8LZDW474NX3D956GGVFAZRQTAE3F4
|
||||
```
|
||||
|
||||
Alternatively, you can provide your [age] secret key as an environment variable `SOPS_AGE_KEY`, or in a different file
|
||||
using `SOPS_AGE_KEY_FILE`.
|
||||
For more information see the [SOPS] guide on [encrypting with age].
|
||||
|
||||
!!! note
|
||||
It's safe to add any secrets created by the clan CLI and placed in your repository to version control systems like `git`.
|
||||
|
||||
### Add Your Public Key(s)
|
||||
|
||||
```console
|
||||
clan secrets users add $USER --age-key <your_public_key>
|
||||
```
|
||||
|
||||
It's best to choose the same username as on your Setup/Admin Machine that you use to control the deployment with.
|
||||
|
||||
Once run this will create the following files:
|
||||
|
||||
```{.console, .no-copy}
|
||||
sops/
|
||||
└── users/
|
||||
└── <your_username>/
|
||||
└── key.json
|
||||
```
|
||||
If you followed the quickstart tutorial all necessary secrets are initialized at this point.
|
||||
|
||||
!!! note
|
||||
You can add multiple age keys for a user by providing multiple `--age-key <your_public_key>` flags:
|
||||
|
||||
```console
|
||||
clan secrets users add $USER \
|
||||
--age-key <your_public_key_1> \
|
||||
--age-key <your_public_key_2> \
|
||||
...
|
||||
```
|
||||
|
||||
### Manage Your Public Key(s)
|
||||
|
||||
You can list keys for your user with `clan secrets users get $USER`:
|
||||
|
||||
```console
|
||||
clan secrets users get alice
|
||||
|
||||
[
|
||||
{
|
||||
"publickey": "age1hrrcspp645qtlj29krjpq66pqg990ejaq0djcms6y6evnmgglv5sq0gewu",
|
||||
"type": "age",
|
||||
"username": "alice"
|
||||
},
|
||||
{
|
||||
"publickey": "age13kh4083t3g4x3ktr52nav6h7sy8ynrnky2x58pyp96c5s5nvqytqgmrt79",
|
||||
"type": "age",
|
||||
"username": "alice"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
To add a new key to your user:
|
||||
|
||||
```console
|
||||
clan secrets users add-key $USER --age-key <your_public_key>
|
||||
```
|
||||
|
||||
To remove a key from your user:
|
||||
|
||||
```console
|
||||
clan secrets users remove-key $USER --age-key <your_public_key>
|
||||
```
|
||||
|
||||
[age]: https://github.com/FiloSottile/age
|
||||
[age plugin]: https://github.com/FiloSottile/awesome-age?tab=readme-ov-file#plugins
|
||||
[sops]: https://github.com/getsops/sops
|
||||
[encrypting with age]: https://github.com/getsops/sops?tab=readme-ov-file#encrypting-using-age
|
||||
|
||||
## Further: Using Age Plugins
|
||||
|
||||
If you wish to use a key generated using an [age plugin] as your admin key, extra care is needed.
|
||||
|
||||
You must **precede your secret key with a comment that contains its corresponding recipient**.
|
||||
|
||||
This is usually output as part of the generation process
|
||||
and is only required because there is no unified mechanism for recovering a recipient from a plugin secret key.
|
||||
|
||||
Here is an example:
|
||||
|
||||
```title="~/.config/sops/age/keys.txt"
|
||||
# public key: age1zdy49ek6z60q9r34vf5mmzkx6u43pr9haqdh5lqdg7fh5tpwlfwqea356l
|
||||
AGE-PLUGIN-FIDO2-HMAC-1QQPQZRFR7ZZ2WCV...
|
||||
```
|
||||
|
||||
!!! note
|
||||
The comment that precedes the plugin secret key need only contain the recipient.
|
||||
Any other text is ignored.
|
||||
|
||||
In the example above, you can specify `# recipient: age1zdy...`, `# public: age1zdy....` or even
|
||||
just `# age1zdy....`
|
||||
|
||||
You will need to add an entry into your `flake.nix` to ensure that the necessary `age` plugins
|
||||
are loaded when using Clan:
|
||||
|
||||
```nix title="flake.nix"
|
||||
{
|
||||
inputs.clan-core.url = "https://git.clan.lol/clan/clan-core/archive/main.tar.gz";
|
||||
inputs.nixpkgs.follows = "clan-core/nixpkgs";
|
||||
|
||||
outputs =
|
||||
{ self, clan-core, ... }:
|
||||
let
|
||||
# Sometimes this attribute set is defined in clan.nix
|
||||
clan = clan-core.lib.clan {
|
||||
inherit self;
|
||||
|
||||
meta.name = "myclan";
|
||||
|
||||
# Add Yubikey and FIDO2 HMAC plugins
|
||||
# Note: the plugins listed here must be available in nixpkgs.
|
||||
secrets.age.plugins = [
|
||||
"age-plugin-yubikey"
|
||||
"age-plugin-fido2-hmac"
|
||||
];
|
||||
|
||||
machines = {
|
||||
# elided for brevity
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
inherit (clan) nixosConfigurations nixosModules clanInternals;
|
||||
|
||||
# elided for brevity
|
||||
};
|
||||
}
|
||||
```
|
||||
@@ -1,25 +1,141 @@
|
||||
If you want to know more about how to save and share passwords in your clan read further!
|
||||
This article provides an overview over the underlying secrets system which is used by [Vars](../guides/vars-backend.md).
|
||||
Under most circumstances you should use [Vars](../guides/vars-backend.md) directly instead.
|
||||
|
||||
### Adding a Secret
|
||||
Consider using `clan secrets` only for managing admin users and groups, as well as a debugging tool.
|
||||
|
||||
Manually interacting with secrets via `clan secrets [set|remove]`, etc may break the integrity of your `Vars` state.
|
||||
|
||||
---
|
||||
|
||||
Clan enables encryption of secrets (such as passwords & keys) ensuring security and ease-of-use among users.
|
||||
|
||||
By default, Clan uses the [sops](https://github.com/getsops/sops) format
|
||||
and integrates with [sops-nix](https://github.com/Mic92/sops-nix) on NixOS machines.
|
||||
Clan can also be configured to be used with other secret store [backends](../reference/clan.core/vars.md#clan.core.vars.settings.secretStore).
|
||||
|
||||
## Create Your Admin Keypair
|
||||
|
||||
To get started, you'll need to create **your admin keypair**.
|
||||
|
||||
!!! info
|
||||
Don't worry — if you've already made one before, this step won't change or overwrite it.
|
||||
|
||||
```bash
|
||||
clan secrets key generate
|
||||
```
|
||||
|
||||
**Output**:
|
||||
|
||||
```{.console, .no-copy}
|
||||
Public key: age1wkth7uhpkl555g40t8hjsysr20drq286netu8zptw50lmqz7j95sw2t3l7
|
||||
|
||||
Generated age private key at '/home/joerg/.config/sops/age/keys.txt' for your user. Please back it up on a secure location or you will lose access to your secrets.
|
||||
Also add your age public key to the repository with 'clan secrets users add YOUR_USER age1wkth7uhpkl555g40t8hjsysr20drq286netu8zptw50lmqz7j95sw2t3l7' (replace YOUR_USER with your actual username)
|
||||
```
|
||||
|
||||
!!! warning
|
||||
Make sure to keep a safe backup of the private key you've just created.
|
||||
If it's lost, you won't be able to get to your secrets anymore because they all need the admin key to be unlocked.
|
||||
|
||||
If you already have an [age] secret key and want to use that instead, you can simply edit `~/.config/sops/age/keys.txt`:
|
||||
|
||||
```title="~/.config/sops/age/keys.txt"
|
||||
AGE-SECRET-KEY-13GWMK0KNNKXPTJ8KQ9LPSQZU7G3KU8LZDW474NX3D956GGVFAZRQTAE3F4
|
||||
```
|
||||
|
||||
Alternatively, you can provide your [age] secret key as an environment variable `SOPS_AGE_KEY`, or in a different file
|
||||
using `SOPS_AGE_KEY_FILE`.
|
||||
For more information see the [SOPS] guide on [encrypting with age].
|
||||
|
||||
!!! note
|
||||
It's safe to add any secrets created by the clan CLI and placed in your repository to version control systems like `git`.
|
||||
|
||||
## Add Your Public Key(s)
|
||||
|
||||
```console
|
||||
clan secrets users add $USER --age-key <your_public_key>
|
||||
```
|
||||
|
||||
It's best to choose the same username as on your Setup/Admin Machine that you use to control the deployment with.
|
||||
|
||||
Once run this will create the following files:
|
||||
|
||||
```{.console, .no-copy}
|
||||
sops/
|
||||
└── users/
|
||||
└── <your_username>/
|
||||
└── key.json
|
||||
```
|
||||
If you followed the quickstart tutorial all necessary secrets are initialized at this point.
|
||||
|
||||
!!! note
|
||||
You can add multiple age keys for a user by providing multiple `--age-key <your_public_key>` flags:
|
||||
|
||||
```console
|
||||
clan secrets users add $USER \
|
||||
--age-key <your_public_key_1> \
|
||||
--age-key <your_public_key_2> \
|
||||
...
|
||||
```
|
||||
|
||||
## Manage Your Public Key(s)
|
||||
|
||||
You can list keys for your user with `clan secrets users get $USER`:
|
||||
|
||||
```console
|
||||
clan secrets users get alice
|
||||
|
||||
[
|
||||
{
|
||||
"publickey": "age1hrrcspp645qtlj29krjpq66pqg990ejaq0djcms6y6evnmgglv5sq0gewu",
|
||||
"type": "age",
|
||||
"username": "alice"
|
||||
},
|
||||
{
|
||||
"publickey": "age13kh4083t3g4x3ktr52nav6h7sy8ynrnky2x58pyp96c5s5nvqytqgmrt79",
|
||||
"type": "age",
|
||||
"username": "alice"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
To add a new key to your user:
|
||||
|
||||
```console
|
||||
clan secrets users add-key $USER --age-key <your_public_key>
|
||||
```
|
||||
|
||||
To remove a key from your user:
|
||||
|
||||
```console
|
||||
clan secrets users remove-key $USER --age-key <your_public_key>
|
||||
```
|
||||
|
||||
[age]: https://github.com/FiloSottile/age
|
||||
[age plugin]: https://github.com/FiloSottile/awesome-age?tab=readme-ov-file#plugins
|
||||
[sops]: https://github.com/getsops/sops
|
||||
[encrypting with age]: https://github.com/getsops/sops?tab=readme-ov-file#encrypting-using-age
|
||||
|
||||
## Adding a Secret
|
||||
|
||||
```shellSession
|
||||
clan secrets set mysecret
|
||||
Paste your secret:
|
||||
```
|
||||
|
||||
### Retrieving a Stored Secret
|
||||
## Retrieving a Stored Secret
|
||||
|
||||
```bash
|
||||
clan secrets get mysecret
|
||||
```
|
||||
|
||||
### List all Secrets
|
||||
## List all Secrets
|
||||
|
||||
```bash
|
||||
clan secrets list
|
||||
```
|
||||
|
||||
### NixOS integration
|
||||
## NixOS integration
|
||||
|
||||
A NixOS machine will automatically import all secrets that are encrypted for the
|
||||
current machine. At runtime it will use the host key to decrypt all secrets into
|
||||
@@ -37,7 +153,7 @@ In your nixos configuration you can get a path to secrets like this `config.sops
|
||||
}
|
||||
```
|
||||
|
||||
### Assigning Access
|
||||
## Assigning Access
|
||||
|
||||
When using `clan secrets set <secret>` without arguments, secrets are encrypted for the key of the user named like your current $USER.
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ from clan_lib.clan.create import CreateOptions, create_clan
|
||||
from clan_lib.errors import ClanError
|
||||
|
||||
from clan_cli.completions import add_dynamic_completer, complete_templates_clan
|
||||
from clan_cli.vars.keygen import create_secrets_user_auto
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -43,6 +44,12 @@ def register_create_parser(parser: argparse.ArgumentParser) -> None:
|
||||
default=False,
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--user",
|
||||
help="The user to generate the keys for. Default: logged-in OS username (e.g. from $LOGNAME or system)",
|
||||
default=None,
|
||||
)
|
||||
|
||||
def create_flake_command(args: argparse.Namespace) -> None:
|
||||
# Ask for a path interactively if none provided
|
||||
if args.name is None:
|
||||
@@ -62,5 +69,10 @@ def register_create_parser(parser: argparse.ArgumentParser) -> None:
|
||||
update_clan=not args.no_update,
|
||||
)
|
||||
)
|
||||
create_secrets_user_auto(
|
||||
flake_dir=Path(args.name).resolve(),
|
||||
user=args.user,
|
||||
force=True,
|
||||
)
|
||||
|
||||
parser.set_defaults(func=create_flake_command)
|
||||
|
||||
@@ -285,7 +285,7 @@ Examples:
|
||||
$ clan secrets get [SECRET]
|
||||
Will display the content of the specified secret.
|
||||
|
||||
For more detailed information, visit: {help_hyperlink("secrets", "https://docs.clan.lol/guides/getting-started/secrets")}
|
||||
For more detailed information, visit: {help_hyperlink("secrets", "https://docs.clan.lol/guides/secrets")}
|
||||
"""
|
||||
),
|
||||
formatter_class=argparse.RawTextHelpFormatter,
|
||||
@@ -324,7 +324,7 @@ Examples:
|
||||
This is especially useful for resetting certain passwords while leaving the rest
|
||||
of the facts for a machine in place.
|
||||
|
||||
For more detailed information, visit: {help_hyperlink("secrets", "https://docs.clan.lol/guides/getting-started/secrets")}
|
||||
For more detailed information, visit: {help_hyperlink("secrets", "https://docs.clan.lol/guides/secrets")}
|
||||
"""
|
||||
),
|
||||
formatter_class=argparse.RawTextHelpFormatter,
|
||||
@@ -362,7 +362,7 @@ Examples:
|
||||
This is especially useful for resetting certain passwords while leaving the rest
|
||||
of the vars for a machine in place.
|
||||
|
||||
For more detailed information, visit: {help_hyperlink("secrets", "https://docs.clan.lol/guides/getting-started/secrets")}
|
||||
For more detailed information, visit: {help_hyperlink("secrets", "https://docs.clan.lol/guides/secrets")}
|
||||
"""
|
||||
),
|
||||
formatter_class=argparse.RawTextHelpFormatter,
|
||||
|
||||
@@ -31,7 +31,7 @@ Examples:
|
||||
Will check facts for the specified machine.
|
||||
|
||||
|
||||
For more detailed information, visit: {help_hyperlink("secrets", "https://docs.clan.lol/guides/getting-started/secrets")}
|
||||
For more detailed information, visit: {help_hyperlink("secrets", "https://docs.clan.lol/guides/secrets")}
|
||||
"""
|
||||
),
|
||||
formatter_class=argparse.RawTextHelpFormatter,
|
||||
@@ -61,7 +61,7 @@ Examples:
|
||||
Will list facts for the specified machine.
|
||||
|
||||
|
||||
For more detailed information, visit: {help_hyperlink("secrets", "https://docs.clan.lol/guides/getting-started/secrets")}
|
||||
For more detailed information, visit: {help_hyperlink("secrets", "https://docs.clan.lol/guides/secrets")}
|
||||
"""
|
||||
),
|
||||
formatter_class=argparse.RawTextHelpFormatter,
|
||||
@@ -101,7 +101,7 @@ Examples:
|
||||
This is especially useful for resetting certain passwords while leaving the rest
|
||||
of the facts for a machine in place.
|
||||
|
||||
For more detailed information, visit: {help_hyperlink("secrets", "https://docs.clan.lol/guides/getting-started/secrets")}
|
||||
For more detailed information, visit: {help_hyperlink("secrets", "https://docs.clan.lol/guides/secrets")}
|
||||
"""
|
||||
),
|
||||
formatter_class=argparse.RawTextHelpFormatter,
|
||||
@@ -125,7 +125,7 @@ Examples:
|
||||
$ clan facts upload [MACHINE]
|
||||
Will upload secrets to a specific machine.
|
||||
|
||||
For more detailed information, visit: {help_hyperlink("secrets", "https://docs.clan.lol/guides/getting-started/secrets")}
|
||||
For more detailed information, visit: {help_hyperlink("secrets", "https://docs.clan.lol/guides/secrets")}
|
||||
"""
|
||||
),
|
||||
formatter_class=argparse.RawTextHelpFormatter,
|
||||
|
||||
@@ -29,7 +29,7 @@ def generate_key() -> sops.SopsKey:
|
||||
path = default_admin_private_key_path()
|
||||
_, pub_key = generate_private_key(out_file=path)
|
||||
print(
|
||||
f"Generated age private key at '{path}' for your user. Please back it up on a secure location or you will lose access to your secrets."
|
||||
f"Generated age private key at '{path}' for your user.\nPlease back it up on a secure location or you will lose access to your secrets."
|
||||
)
|
||||
return sops.SopsKey(pub_key, username="", key_type=sops.KeyType.AGE)
|
||||
|
||||
|
||||
@@ -421,7 +421,7 @@ def ensure_admin_public_keys(flake_dir: Path) -> set[SopsKey]:
|
||||
f"- {'\n- '.join(f'{key.key_type.name.lower()}: {key.pubkey}' for key in keys)}\n\n"
|
||||
f"Please ensure you have created a Clan secrets user and added one of your SOPS keys"
|
||||
f"to that user.\n"
|
||||
f"For more information, see: https://docs.clan.lol/guides/getting-started/secrets/#add-your-public-keys"
|
||||
f"For more information, see: https://docs.clan.lol/guides/secrets/#add-your-public-keys"
|
||||
)
|
||||
raise ClanError(msg)
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import logging
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from clan_cli.tests.fixtures_flakes import FlakeForTest, substitute
|
||||
from clan_cli.tests.fixtures_flakes import substitute
|
||||
from clan_cli.tests.helpers import cli
|
||||
from clan_cli.tests.stdout import CaptureOutput
|
||||
from clan_lib.cmd import run
|
||||
@@ -21,6 +21,7 @@ def test_create_flake(
|
||||
) -> None:
|
||||
flake_dir = temporary_home / "test-flake"
|
||||
|
||||
monkeypatch.setenv("LOGNAME", "testuser")
|
||||
cli.run(["flakes", "create", str(flake_dir), "--template=default", "--no-update"])
|
||||
|
||||
# Replace the inputs.clan.url in the template flake.nix
|
||||
@@ -65,6 +66,7 @@ def test_create_flake_existing_git(
|
||||
|
||||
run(["git", "init", str(temporary_home)])
|
||||
|
||||
monkeypatch.setenv("LOGNAME", "testuser")
|
||||
cli.run(["flakes", "create", str(flake_dir), "--template=default", "--no-update"])
|
||||
|
||||
# Replace the inputs.clan.url in the template flake.nix
|
||||
@@ -101,12 +103,12 @@ def test_create_flake_existing_git(
|
||||
def test_ui_template(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
temporary_home: Path,
|
||||
test_flake_with_core: FlakeForTest,
|
||||
clan_core: Path,
|
||||
capture_output: CaptureOutput,
|
||||
) -> None:
|
||||
flake_dir = temporary_home / "test-flake"
|
||||
|
||||
monkeypatch.setenv("LOGNAME", "testuser")
|
||||
cli.run(["flakes", "create", str(flake_dir), "--template=minimal", "--no-update"])
|
||||
|
||||
# Replace the inputs.clan.url in the template flake.nix
|
||||
|
||||
@@ -197,7 +197,7 @@ Examples:
|
||||
$ clan vars upload [MACHINE]
|
||||
Will upload secrets to a specific machine.
|
||||
|
||||
For more detailed information, visit: {help_hyperlink("secrets", "https://docs.clan.lol/guides/getting-started/secrets")}
|
||||
For more detailed information, visit: {help_hyperlink("secrets", "https://docs.clan.lol/guides/secrets")}
|
||||
"""
|
||||
),
|
||||
formatter_class=argparse.RawTextHelpFormatter,
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import argparse
|
||||
import getpass
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from clan_cli.secrets.key import generate_key
|
||||
from clan_cli.secrets.sops import maybe_get_admin_public_keys
|
||||
from clan_cli.secrets.sops import SopsKey, maybe_get_admin_public_keys
|
||||
from clan_cli.secrets.users import add_user
|
||||
from clan_lib.api import API
|
||||
from clan_lib.errors import ClanError
|
||||
@@ -12,6 +13,17 @@ from clan_lib.errors import ClanError
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _get_user_or_default(user: str | None) -> str:
|
||||
"""Get the user name, defaulting to the logged-in OS username if not provided."""
|
||||
if user is None:
|
||||
try:
|
||||
user = getpass.getuser()
|
||||
except Exception as e:
|
||||
msg = "No user provided and could not determine logged-in OS username. Please provide an explicit username via argument"
|
||||
raise ClanError(msg) from e
|
||||
return user
|
||||
|
||||
|
||||
# TODO: Unify with "create clan" should be done automatically
|
||||
@API.register
|
||||
def create_secrets_user(
|
||||
@@ -20,11 +32,7 @@ def create_secrets_user(
|
||||
"""
|
||||
initialize sops keys for vars
|
||||
"""
|
||||
if user is None:
|
||||
user = os.getenv("USER", None)
|
||||
if not user:
|
||||
msg = "No user provided and environment variable: '$USER' is not set. Please provide an explizit username via argument"
|
||||
raise ClanError(msg)
|
||||
user = _get_user_or_default(user)
|
||||
pub_keys = maybe_get_admin_public_keys()
|
||||
if not pub_keys:
|
||||
pub_keys = [generate_key()]
|
||||
@@ -37,10 +45,97 @@ def create_secrets_user(
|
||||
)
|
||||
|
||||
|
||||
def _select_keys_interactive(pub_keys: list[SopsKey]) -> list[SopsKey]:
|
||||
# let the user select which of the keys to use
|
||||
log.info("\nFound existing admin keys on this machine:")
|
||||
selected_keys: list[SopsKey] = []
|
||||
for i, key in enumerate(pub_keys):
|
||||
log.info(f"{i + 1}: {key}")
|
||||
while not selected_keys:
|
||||
choice = input(
|
||||
"Select keys to use (comma-separated list of numbers, or leave empty to select all): "
|
||||
).strip()
|
||||
if not choice:
|
||||
log.info("No keys selected, using all keys.")
|
||||
return pub_keys
|
||||
|
||||
try:
|
||||
indices = [int(i) - 1 for i in choice.split(",")]
|
||||
selected_keys = [pub_keys[i] for i in indices if 0 <= i < len(pub_keys)]
|
||||
except ValueError:
|
||||
log.info("Invalid input. Please enter a comma-separated list of numbers.")
|
||||
|
||||
return selected_keys
|
||||
|
||||
|
||||
def create_secrets_user_interactive(
|
||||
flake_dir: Path, user: str | None = None, force: bool = False
|
||||
) -> None:
|
||||
"""
|
||||
Initialize sops keys for vars interactively.
|
||||
"""
|
||||
user = _get_user_or_default(user)
|
||||
pub_keys = maybe_get_admin_public_keys()
|
||||
if pub_keys:
|
||||
# let the user select which of the keys to use
|
||||
pub_keys = _select_keys_interactive(pub_keys)
|
||||
else:
|
||||
log.info(
|
||||
"\nNo admin keys found on this machine, generating a new key for sops."
|
||||
)
|
||||
pub_keys = [generate_key()]
|
||||
# make sure the user backups the generated key
|
||||
log.info("\n⚠️ IMPORTANT: Secret Key Backup ⚠️")
|
||||
log.info(
|
||||
"The generated key above is CRITICAL for accessing your clan's secrets."
|
||||
)
|
||||
log.info("Without this key, you will lose access to all encrypted data!")
|
||||
log.info("Please backup the key file immediately to a secure location.")
|
||||
log.info("The key is typically stored in ~/.config/sops/age/keys.txt")
|
||||
confirm = None
|
||||
while not confirm or confirm.lower() != "y":
|
||||
log.info(
|
||||
"\nI have backed up the key file to a secure location. Confirm [y/N]: "
|
||||
)
|
||||
confirm = input().strip().lower()
|
||||
if confirm != "y":
|
||||
log.error(
|
||||
"You must backup the key before proceeding. This is critical for data recovery!"
|
||||
)
|
||||
|
||||
# persist the generated or chosen admin pubkey in the repo
|
||||
add_user(
|
||||
flake_dir=flake_dir,
|
||||
name=user,
|
||||
keys=pub_keys,
|
||||
force=force,
|
||||
)
|
||||
|
||||
|
||||
def create_secrets_user_auto(
|
||||
flake_dir: Path, user: str | None = None, force: bool = False
|
||||
) -> None:
|
||||
"""
|
||||
Detect if the user is in interactive mode or not and choose the appropriate routine.
|
||||
"""
|
||||
if sys.stdin.isatty():
|
||||
create_secrets_user_interactive(
|
||||
flake_dir=flake_dir,
|
||||
user=user,
|
||||
force=force,
|
||||
)
|
||||
else:
|
||||
create_secrets_user(
|
||||
flake_dir=flake_dir,
|
||||
user=user,
|
||||
force=force,
|
||||
)
|
||||
|
||||
|
||||
def _command(
|
||||
args: argparse.Namespace,
|
||||
) -> None:
|
||||
create_secrets_user(
|
||||
create_secrets_user_auto(
|
||||
flake_dir=args.flake.path,
|
||||
user=args.user,
|
||||
force=args.force,
|
||||
@@ -50,7 +145,7 @@ def _command(
|
||||
def register_keygen_parser(parser: argparse.ArgumentParser) -> None:
|
||||
parser.add_argument(
|
||||
"--user",
|
||||
help="The user to generate the keys for. Default: $USER",
|
||||
help="The user to generate the keys for. Default: logged-in OS username (e.g. from $LOGNAME or system)",
|
||||
default=None,
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user