git.delta.rocks / jrsonnet / refs/commits / 5a5b360a3403

difftreelog

feat unify shared and host secret handling

xwkwvyrvYaroslav Bolyukin2026-01-22parent: #20a41a3.patch.diff
in: trunk

8 files changed

modifiedcmds/fleet/src/cmds/secrets/mod.rsdiffbeforeafterboth
8use chrono::{DateTime, Utc};8use chrono::{DateTime, Utc};
9use clap::Parser;9use clap::Parser;
10use fleet_base::{10use fleet_base::{
11 fleetdata::{11 fleetdata::{FleetSecretData, FleetSecretDistribution, FleetSecretPart, encrypt_secret_data},
12 FleetHostSecret, FleetSecretData, FleetSecretPart, FleetSharedSecret, encrypt_secret_data,
13 },
14 host::Config,12 host::Config,
15 opts::FleetOpts,13 opts::FleetOpts,
16 secret::{Expectations, RegenerationReason, SharedSecretDefinition, secret_needs_regeneration},14 secret::{Expectations, RegenerationReason, SharedSecretDefinition, secret_needs_regeneration},
28 AddManager,26 AddManager,
29 /// Force load host keys for all defined hosts27 /// Force load host keys for all defined hosts
30 ForceKeys,28 ForceKeys,
31 /// Add secret, data should be provided in stdin29 /// Read secret from remote host, requires sudo on one of the owning hosts
32 AddShared {30 Read {
33 /// Secret name31 /// Secret name to read
34 name: String,32 name: String,
35 /// Secret owners
36 #[clap(long, short)]
37 machines: Vec<String>,
38 /// Override secret if already present
39 #[clap(long)]
40 force: bool,
41 /// Secret public part
42 #[clap(long)]
43 public: Option<String>,
44 /// Load public part from specified file
45 #[clap(long)]
46 public_file: Option<PathBuf>,
4733
48 /// Create a notification on secret expiration34 /// Distribution with what machine to read
49 #[clap(long)]
50 expires_at: Option<DateTime<Utc>>,
51
52 /// Secret with this name already exists, override its value while keeping the same owners.
53 #[clap(long)]
54 re_add: bool,
55
56 /// How to name public secret part
57 #[clap(long, short = 'p', default_value = "public")]35 /// If not shared between multiple - defaults to single owner
58 public_part: String,
59 /// How to name private secret part
60 #[clap(short = 's', long, default_value = "secret")]
61 part: String,
62 },
63 /// Add secret, data should be provided in stdin
64 Add {
65 /// Secret name
66 name: String,
67 /// Secret owner
68 #[clap(short = 'm', long)]36 #[clap(short = 'm', long)]
69 machine: String,37 machine: Option<String>,
70 /// Replace secret if already present
71 #[clap(long)]
72 replace: bool,
73 /// Add new parts to existing secret
74 #[clap(long)]
75 merge: bool,
76 /// Secret public part
77 #[clap(long)]
78 public: Option<String>,
79 /// Load public part from specified file
80 #[clap(long)]
81 public_file: Option<PathBuf>,
8238
83 /// How to name public secret part
84 #[clap(short = 'p', long, default_value = "public")]
85 public_part: String,
86 /// How to name private secret part
87 #[clap(short = 's', long, default_value = "secret")]
88 part: String,
89 },
90 /// Read secret from remote host, requires sudo on said host
91 Read {
92 name: String,
93 #[clap(short = 'm', long)]
94 machine: String,
95
96 /// Which private secret part to read39 /// Which private secret part to read
97 #[clap(short = 'p', long, default_value = "secret")]40 #[clap(short = 'p', long, default_value = "secret")]
98 part: String,41 part: String,
99 },42
100 /// Read secret from remote host, requires sudo on said host
101 ReadShared {
102 name: String,
103 /// Which private secret part to read
104 #[clap(short = 'p', long, default_value = "secret")]
105 part: String,
106 /// Which host should we use to decrypt, in case if reencryption is required, without43 /// Which host should we use to decrypt, in case if reencryption is required, without
107 /// regeneration44 /// regeneration
108 #[clap(long)]45 #[clap(long)]
109 prefer_identities: Vec<String>,46 prefer_identities: Vec<String>,
110 },47 },
111 UpdateShared {
112 name: String,
113
114 #[clap(short = 'm', long)]
115 machine: Option<Vec<String>>,
116
117 #[clap(long)]
118 add_machine: Vec<String>,
119 #[clap(long)]
120 remove_machine: Vec<String>,
121
122 /// Which host should we use to decrypt
123 #[clap(long)]
124 prefer_identities: Vec<String>,
125 },
126 Regenerate {48 Regenerate {
127 /// Which host should we use to decrypt, in case if reencryption is required, without49 /// Which host should we use to decrypt, in case if reencryption is required, without
128 /// regeneration50 /// regeneration
152async fn maybe_regenerate_shared_secret(74async fn maybe_regenerate_shared_secret(
153 secret_name: &str,75 secret_name: &str,
154 config: &Config,76 config: &Config,
155 mut secret: FleetSharedSecret,77 mut secret: FleetSecretDistribution,
156 definition: SharedSecretDefinition,78 definition: SharedSecretDefinition,
157 prefer_identities: &[String],79 prefer_identities: &[String],
158 expectations: &Expectations,80 expectations: &Expectations,
159) -> Result<FleetSharedSecret> {81) -> Result<FleetSecretDistribution> {
160 let reason = secret_needs_regeneration(&secret.secret, &secret.owners, expectations);82 let reason = secret_needs_regeneration(&secret.secret, &secret.owners, expectations);
161 let value = definition.definition_value();83 let value = definition.definition_value();
16284
397 display_name: &str,319 display_name: &str,
398 secret: SharedSecretDefinition,320 secret: SharedSecretDefinition,
399 expectations: &Expectations,321 expectations: &Expectations,
400) -> Result<FleetSharedSecret> {322) -> Result<FleetSecretDistribution> {
401 // let owners: Vec<String> = nix_go_json!(secret.expectedOwners);323 // let owners: Vec<String> = nix_go_json!(secret.expectedOwners);
402 Ok(FleetSharedSecret {324 Ok(FleetSecretDistribution {
403 managed: Some(true),325 managed: Some(true),
404 secret: generate(326 secret: generate(
405 config,327 config,
504 config.key(&host.name).await?;426 config.key(&host.name).await?;
505 }427 }
506 }428 }
507 Secret::AddShared {429 Secret::Read {
508 machines,
509 name,430 name,
510 force,431 machine,
511 public,
512 public_part: public_name,
513 public_file,
514 expires_at,
515 re_add,
516 part: part_name,432 part: part_name,
433 mut prefer_identities,
517 } => {434 } => {
518 let mut machines: BTreeSet<String> = machines.into_iter().collect();435 let Some(secret) = config.shared_secret(&name) else {
519 // TODO: Forbid updating secrets with set expectedOwners (= not user-managed).
520
521 if let Some(old_shared) = config.shared_secret(&name)? {
522 if !force && !re_add {
523 bail!("secret already defined");
524 };
525 if old_shared.managed.unwrap_or(false) {
526 bail!("secret is marked as managed, should not be updated manually");
527 };
528 if re_add {
529 // Fixme: use clap to limit this usage
530 ensure!(!force, "--force and --readd are not compatible");
531 ensure!(
532 machines.is_empty(),
533 "you can't use machines argument for --readd"
534 );
535 machines = old_shared.owners;
536 }
537 } else if re_add {
538 bail!("secret doesn't exists");436 bail!("secret doesn't exists");
539 };437 };
540438
541 let recipients = config439 let dist = if secret.len() == 1 {
542 .recipients(machines.iter().cloned().collect())
543 .await?;
544
545 let mut parts = BTreeMap::new();
546
547 let mut input = vec![];
548 io::stdin().read_to_end(&mut input)?;
549
550 if !input.is_empty() {
551 let encrypted = encrypt_secret_data(recipients.iter(), input)440 &secret[0]
552 .ok_or_else(|| anyhow!("no recipients provided"))?;441 } else if let Some(machine) = machine {
553 parts.insert(part_name, FleetSecretPart { raw: encrypted });
554 }
555
556 if let Some(public) = parse_public(public, public_file).await? {
557 parts.insert(public_name, FleetSecretPart { raw: public });442 let dist = secret.get(&machine);
558 }443 let Some(dist) = dist else {
559
560 config.replace_shared(
561 name,
562 FleetSharedSecret {
563 managed: Some(false),
564 owners: machines,
565 secret: FleetSecretData {
566 created_at: Utc::now(),444 bail!("machine {machine} has no distribution of secret {name}");
567 expires_at,
568 parts,
569 generation_data: serde_json::Value::Null,
570 },
571 },
572 );
573 }
574 Secret::Add {
575 machine,
576 name,
577 replace,
578 merge,
579 public,
580 public_part: public_name,
581 public_file,
582 part: part_name,
583 } => {
584 if config.has_secret(&machine, &name) && !replace && !merge {
585 bail!(
586 "secret already defined.\nUse --replace to override, or --merge to add new parts to existing secret"
587 );
588 }445 };
589
590 let mut out = if merge && !replace {446 prefer_identities.push(machine);
591 config
592 .host_secret(&machine, &name)
593 .context("failed to read existing secret for --merge")?447 dist
594 } else {448 } else {
595 FleetHostSecret {449 bail!(
596 managed: Some(false),
597 secret: FleetSecretData {450 "secret {name} has shares, but no --machine specified for specifing which do you need"
598 created_at: Utc::now(),
599 expires_at: None,
600 parts: BTreeMap::new(),
601 generation_data: serde_json::Value::Null,
602 },451 )
603 }
604 };452 };
605 if out.managed.unwrap_or(false) {
606 bail!("secret is managed by fleet and should not be updated manually");
607 }
608 out.managed = Some(false);
609453
610 if let Some(secret) = parse_secret().await? {454 let Some(part) = dist.secret.parts.get(&part_name) else {
611 let recipient = config.recipient(&machine).await?;
612 let encrypted =
613 encrypt_secret_data([&recipient], secret).expect("recipient provided");
614 if out
615 .secret
616 .parts
617 .insert(part_name.clone(), FleetSecretPart { raw: encrypted })
618 .is_some() && !replace
619 {
620 bail!(
621 "part {part_name:?} is already defined, use --replace if you wish to replace it"
622 );
623 }
624 }
625
626 if let Some(public) = parse_public(public, public_file).await? {
627 if out
628 .secret
629 .parts
630 .insert(public_name.clone(), FleetSecretPart { raw: public })
631 .is_some() && !replace
632 {
633 bail!(
634 "part {public_name:?} is already defined, use --replace if you wish to replace it"
635 );
636 }
637 };
638
639 config.insert_secret(&machine, name, out);
640 }
641 #[allow(clippy::await_holding_refcell_ref)]
642 Secret::Read {
643 name,
644 machine,
645 part: part_name,
646 } => {
647 let secret = config.host_secret(&machine, &name)?;
648 let Some(secret) = secret.secret.parts.get(&part_name) else {
649 bail!("no part {part_name} in secret {name}");455 bail!("no part {part_name} in secret {name}");
650 };456 };
651 let data = if secret.raw.encrypted {
652 let host = config.host(&machine).await?;
653 host.decrypt(secret.raw.clone()).await?
654 } else {
655 secret.raw.data.clone()
656 };
657
658 stdout().write_all(&data)?;
659 }
660 Secret::ReadShared {
661 name,
662 part: part_name,
663 prefer_identities,
664 } => {
665 let Some(secret) = config.shared_secret(&name)? else {
666 bail!("secret doesn't exists");
667 };
668 let Some(part) = secret.secret.parts.get(&part_name) else {
669 bail!("no part {part_name} in secret {name}");
670 };
671 let data = if part.raw.encrypted {457 let data = if part.raw.encrypted {
672 let identity_holder = if !prefer_identities.is_empty() {458 let identity_holder = if !prefer_identities.is_empty() {
673 prefer_identities459 prefer_identities
674 .iter()460 .iter()
675 .find(|i| secret.owners.iter().any(|s| s == *i))461 .find(|i| dist.owners.iter().any(|s| s == *i))
676 } else {462 } else {
677 secret.owners.first()463 dist.owners.first()
678 };464 };
679 let Some(identity_holder) = identity_holder else {465 let Some(identity_holder) = identity_holder else {
680 bail!("no available holder found");466 bail!("no available holder found");
686 };472 };
687 stdout().write_all(&data)?;473 stdout().write_all(&data)?;
688 }474 }
689 Secret::UpdateShared {
690 name,
691 machine,
692 add_machine,
693 remove_machine,
694 prefer_identities,
695 } => {
696 // TODO: Forbid updating secrets with set expectedOwners (= not user-managed).
697
698 let Some(secret) = config.shared_secret(&name)? else {
699 bail!("secret doesn't exists");
700 };
701 if secret.secret.parts.values().all(|v| !v.raw.encrypted) {
702 bail!("no secret");
703 }
704
705 let initial_machines = secret.owners.clone();
706 let target_machines = parse_machines(
707 initial_machines.clone(),
708 machine,
709 add_machine,
710 remove_machine,
711 )?;
712
713 if target_machines.is_empty() {
714 info!("no machines left for secret, removing it");
715 config.remove_shared(&name);
716 return Ok(());
717 }
718
719 let definition = config.shared_secret_definition(&name)?;
720 let expectations = definition
721 .expectations()
722 .with_context(|| format!("expectations for shared {name:?}"))?;
723
724 let updated = maybe_regenerate_shared_secret(
725 &name,
726 config,
727 secret,
728 definition,
729 &prefer_identities,
730 &expectations,
731 )
732 .await?;
733 config.replace_shared(name, updated);
734 }
735 Secret::Regenerate {475 Secret::Regenerate {
736 prefer_identities,476 prefer_identities,
737 skip_hosts,477 skip_hosts,
738 } => {478 } => {
739 info!("checking for secrets to regenerate");479 /*
740 let expected_shared_set = config480 info!("checking for secrets to regenerate");
741 .list_configured_shared()481 let expected_shared_set = config
742 .await?482 .list_configured_shared()
743 .into_iter()483 .await?
744 .collect::<HashSet<_>>();484 .into_iter()
745 let stored_shared_set = config.list_shared().into_iter().collect::<HashSet<_>>();485 .collect::<HashSet<_>>();
746 {486 let stored_shared_set = config.list_secrets().into_iter().collect::<HashSet<_>>();
747 // Generate missing shared
748 let _span = info_span!("shared").entered();
749 for missing in expected_shared_set.difference(&stored_shared_set) {
750 let definition = config.shared_secret_definition(missing)?;
751 if !definition.is_managed()? {
752 info!("skipping unmanaged secret: {missing}");
753 continue;
754 }
755 let expectations = definition
756 .expectations()
757 .with_context(|| format!("expectations for shared {missing:?}"))?;
758 info!("generating secret: {missing}");
759 let shared = generate_shared(config, missing, definition, &expectations)
760 .in_current_span()
761 .await?;
762 config.replace_shared(missing.to_string(), shared)
763 }
764 }
765 if !skip_hosts {
766 for host in config.list_hosts().await? {
767 if opts.should_skip(&host).await? {
768 continue;
769 }
770
771 let _span = info_span!("host", host = host.name).entered();
772 let expected_set = host
773 .list_defined_secrets()?
774 .into_iter()
775 .collect::<HashSet<_>>();
776 let stored_set = config
777 .list_secrets(&host.name)
778 .into_iter()
779 .collect::<HashSet<_>>();
780 for missing_secret in expected_set.difference(&stored_set) {
781 let secret = host.secret_definition(missing_secret)?;
782 if secret.is_shared()? {
783 continue;
784 }
785 info!("generating missing secret: {missing_secret}");
786 let expectations = secret.expectations().with_context(|| {
787 format!("expectations for {missing_secret:?} of {:?}", host.name)
788 })?;
789 let generated = match generate(
790 config,
791 missing_secret,
792 secret.definition_value()?,
793 &expectations,
794 )
795 .in_current_span()
796 .await
797 {
798 Ok(v) => v,
799 Err(e) => {
800 error!("{e:?}");
801 continue;
802 }
803 };
804 config.insert_secret(
805 &host.name,
806 missing_secret.to_string(),
807 FleetHostSecret {
808 managed: Some(true),
809 secret: generated,
810 },
811 )
812 }
813 for known_secret in stored_set.intersection(&expected_set) {
814 let secret = host.secret_definition(known_secret)?;
815 if secret.is_shared()? {
816 continue;
817 }
818 info!("updating secret: {known_secret}");
819 let data = config.host_secret(&host.name, known_secret)?;
820 let expectations = secret.expectations()?;
821 if let Some(regen_reason) = data.needs_regeneration(&expectations) {
822 info!("needs regeneration: {regen_reason}");
823 let generated = match generate(
824 config,
825 known_secret,
826 secret.definition_value()?,
827 &expectations,
828 )
829 .in_current_span()
830 .await
831 {487 {
832 Ok(v) => v,488 // Generate missing shared
489 let _span = info_span!("shared").entered();
490 for missing in expected_shared_set.difference(&stored_shared_set) {
491 let definition = config.shared_secret_definition(missing)?;
833 Err(e) => {492 if !definition.is_managed()? {
493 info!("skipping unmanaged secret: {missing}");
834 error!("{e:?}");494 continue;
495 }
496 let expectations = definition
497 .expectations()
498 .with_context(|| format!("expectations for shared {missing:?}"))?;
835 continue;499 info!("generating secret: {missing}");
500 let shared = generate_shared(config, missing, definition, &expectations)
501 .in_current_span()
502 .await?;
503 config.replace_shared(missing.to_string(), shared)
836 }504 }
837 };505 }
838 config.insert_secret(506 if !skip_hosts {
839 &host.name,
840 known_secret.to_string(),
841 FleetHostSecret {
842 managed: Some(true),507 for host in config.list_hosts().await? {
843 secret: generated,
844 },
845 )
846 }
847 }
848 for removed_secret in stored_set.difference(&expected_set) {
849 let definition = host.secret_definition(removed_secret)?;508 if opts.should_skip(&host).await? {
850 if definition.is_shared()? {
851 continue;509 continue;
852 }
853 info!("removing secret: {removed_secret}");
854 config.remove_secret(&host.name, removed_secret);510 }
855 }
856 }
857 }
858 for known_secret in stored_shared_set.intersection(&expected_shared_set) {
859 info!("updating shared secret: {known_secret}");
860 let data = config.shared_secret(known_secret)?.expect("exists");
861511
862 let definition = config.shared_secret_definition(known_secret)?;512 let _span = info_span!("host", host = host.name).entered();
863 let expectations = definition.expectations()?;513 let expected_set = host
864 config.replace_shared(514 .list_defined_secrets()?
865 known_secret.to_owned(),515 .into_iter()
866 maybe_regenerate_shared_secret(516 .collect::<HashSet<_>>();
867 known_secret,517 let stored_set = config
868 config,518 .list_secrets_for_owner(&host.name)
869 data,519 .into_iter()
870 definition,520 .collect::<HashSet<_>>();
871 &prefer_identities,521 for missing_secret in expected_set.difference(&stored_set) {
872 &expectations,522 let secret = host.secret_definition(missing_secret)?;
873 )523 if secret.is_shared()? {
874 .await?,524 continue;
875 );525 }
876 }526 info!("generating missing secret: {missing_secret}");
877 for removed_secret in stored_shared_set.difference(&expected_shared_set) {527 let expectations = secret.expectations().with_context(|| {
878 info!("removing shared secret: {removed_secret}");528 format!("expectations for {missing_secret:?} of {:?}", host.name)
879 config.remove_shared(removed_secret);529 })?;
880 }530 let generated = match generate(
531 config,
532 missing_secret,
533 secret.definition_value()?,
534 &expectations,
535 )
536 .in_current_span()
537 .await
538 {
539 Ok(v) => v,
540 Err(e) => {
541 error!("{e:?}");
542 continue;
543 }
544 };
545 config.insert_secret(host.name, missing_secret.to_string(), generated)
546 }
547 for known_secret in stored_set.intersection(&expected_set) {
548 let secret = host.secret_definition(known_secret)?;
549 if secret.is_shared()? {
550 continue;
551 }
552 info!("updating secret: {known_secret}");
553 let data = config.host_secret(&host.name, known_secret)?;
554 let expectations = secret.expectations()?;
555 if let Some(regen_reason) = data.needs_regeneration(&expectations) {
556 info!("needs regeneration: {regen_reason}");
557 let generated = match generate(
558 config,
559 known_secret,
560 secret.definition_value()?,
561 &expectations,
562 )
563 .in_current_span()
564 .await
565 {
566 Ok(v) => v,
567 Err(e) => {
568 error!("{e:?}");
569 continue;
570 }
571 };
572 config.insert_secret(
573 &host.name,
574 known_secret.to_string(),
575 FleetLegacyHostSecret {
576 managed: Some(true),
577 secret: generated,
578 },
579 )
580 }
581 }
582 for removed_secret in stored_set.difference(&expected_set) {
583 let definition = host.secret_definition(removed_secret)?;
584 if definition.is_shared()? {
585 continue;
586 }
587 info!("removing secret: {removed_secret}");
588 config.remove_secret(&host.name, removed_secret);
589 }
590 }
591 }
592 for known_secret in stored_shared_set.intersection(&expected_shared_set) {
593 info!("updating shared secret: {known_secret}");
594 let data = config.shared_secret(known_secret)?.expect("exists");
595
596 let definition = config.shared_secret_definition(known_secret)?;
597 let expectations = definition.expectations()?;
598 config.replace_shared(
599 known_secret.to_owned(),
600 maybe_regenerate_shared_secret(
601 known_secret,
602 config,
603 data,
604 definition,
605 &prefer_identities,
606 &expectations,
607 )
608 .await?,
609 );
610 }
611 for removed_secret in stored_shared_set.difference(&expected_shared_set) {
612 info!("removing shared secret: {removed_secret}");
613 config.remove_shared(removed_secret);
614 }
615 */
616 todo!()
881 }617 }
882 Secret::List {} => {618 Secret::List {} => {
883 let _span = info_span!("loading secrets").entered();619 let _span = info_span!("loading secrets").entered();
892 let mut table = vec![];628 let mut table = vec![];
893 for name in configured.iter().cloned() {629 for name in configured.iter().cloned() {
894 let config = config.clone();630 let config = config.clone();
895 let data = config.shared_secret(&name)?.expect("exists");631 let data = config.shared_secret(&name).expect("exists");
896 let definition = config.shared_secret_definition(&name)?;632 let definition = config.shared_secret_definition(&name)?;
897 let expectations = definition.expectations()?;633 let expectations = definition.expectations()?;
898 let owners = data634 let owners = data
899 .owners635 .owners()
900 .iter()
901 .map(|o| {636 .map(|o| {
902 if expectations.owners.contains(o) {637 if expectations.owners.contains(o) {
903 o.green().to_string()638 o.green().to_string()
919 part,654 part,
920 add,655 add,
921 } => {656 } => {
922 let secret = config.host_secret(&machine, &name)?;657 let secret = config
658 .host_secret(&machine, &name)
659 .context("secret not found")?;
923 if let Some(data) = secret.secret.parts.get(&part) {660 if let Some(data) = secret.secret.parts.get(&part) {
924 let host = config.host(&machine).await?;661 let host = config.host(&machine).await?;
925 let secret = host.decrypt(data.raw.clone()).await?;662 let secret = host.decrypt(data.raw.clone()).await?;
modifiedcrates/fleet-base/src/fleetdata.rsdiffbeforeafterboth
1use std::{1use std::{
2 collections::{BTreeMap, BTreeSet},2 collections::{
3 BTreeMap, BTreeSet,
4 btree_map::{self, Entry},
5 },
3 io::{self, Cursor},6 io::{self, Cursor},
7 ops::Deref,
4};8};
59
6use age::Recipient;10use age::Recipient;
12};16};
13use serde::{Deserialize, Serialize, de::Error};17use serde::{
18 Deserialize, Serialize,
19 de::{self, Error},
20};
14use serde_json::Value;21use serde_json::Value;
15
16use crate::secret::{Expectations, RegenerationReason, secret_needs_regeneration};22use tracing::info;
1723
18#[derive(Serialize, Deserialize, Default)]24#[derive(Serialize, Deserialize, Default)]
19#[serde(rename_all = "camelCase")]25#[serde(rename_all = "camelCase")]
73 #[serde(default)]79 #[serde(default)]
74 pub hosts: BTreeMap<String, HostData>,80 pub hosts: BTreeMap<String, HostData>,
81
75 #[serde(default)]82 #[serde(default, alias = "shared_secrets")]
76 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
77 pub shared_secrets: BTreeMap<String, FleetSharedSecret>,83 pub secrets: FleetSecrets,
84
85 // extra_name => anything
78 #[serde(default)]86 #[serde(default)]
79 #[serde(skip_serializing_if = "BTreeMap::is_empty")]87 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
80 pub host_secrets: BTreeMap<String, BTreeMap<String, FleetHostSecret>>,88 pub extra: BTreeMap<String, Value>,
8189
82 // extra_name => anything
83 #[serde(default)]90 #[serde(default)]
84 #[serde(skip_serializing_if = "BTreeMap::is_empty")]91 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
85 pub extra: BTreeMap<String, Value>,92 host_secrets: BTreeMap<String, BTreeMap<String, FleetSecretDistribution>>,
86}93}
94impl FleetData {
95 pub fn from_str(s: &str) -> anyhow::Result<Self> {
96 let mut data: Self = nixlike::parse_str(s)?;
97 if !data.host_secrets.is_empty() {
98 info!("migrating host secrets into shared secrets structure");
99 data.secrets
100 .merge_from_hosts(std::mem::take(&mut data.host_secrets));
101 }
102 Ok(data)
103 }
104}
87105
88/// Returns None if recipients.is_empty()106/// Returns None if recipients.is_empty()
89pub fn encrypt_secret_data<'r>(107pub fn encrypt_secret_data<'r>(
129#[derive(Serialize, Deserialize, Clone)]147#[derive(Serialize, Deserialize, Clone)]
130#[serde(rename_all = "camelCase")]148#[serde(rename_all = "camelCase")]
131#[must_use]149#[must_use]
132pub struct FleetHostSecret {150pub struct FleetSecretDistribution {
133 #[serde(default)]151 #[serde(default)]
134 #[serde(skip_serializing_if = "Option::is_none")]152 #[serde(skip_serializing_if = "Option::is_none")]
135 pub managed: Option<bool>,153 pub managed: Option<bool>,
154 #[serde(default)]
155 pub owners: BTreeSet<String>,
136 #[serde(flatten)]156 #[serde(flatten)]
137 pub secret: FleetSecretData,157 pub secret: FleetSecretData,
138}158}
159
160#[derive(Clone)]
161#[must_use]
162pub struct FleetSecretDistributions(Vec<FleetSecretDistribution>);
163
164impl Deref for FleetSecretDistributions {
165 type Target = [FleetSecretDistribution];
166
167 fn deref(&self) -> &Self::Target {
168 self.0.as_slice()
169 }
170}
171
139impl FleetHostSecret {172impl FleetSecretDistributions {
173 pub fn owners(&self) -> impl Iterator<Item = &String> {
174 self.0.iter().flat_map(|v| v.owners.iter())
175 }
176 #[allow(
177 clippy::len_without_is_empty,
178 reason = "should not be empty for a long time"
179 )]
180 pub fn len(&self) -> usize {
181 self.0.len()
182 }
183
140 pub fn needs_regeneration(&self, expectations: &Expectations) -> Option<RegenerationReason> {184 pub fn get(&self, owner: &str) -> Option<&FleetSecretDistribution> {
141 secret_needs_regeneration(&self.secret, &expectations.owners, expectations)185 self.0.iter().find(|d| d.owners.contains(owner))
142 }186 }
187 fn entry(&mut self, owner: String) -> DistEntry<'_> {
188 let Some(idx) = self.0.iter().position(|d| d.owners.contains(&owner)) else {
189 return DistEntry::Vacant(VacantDistEntry {
190 distributions: self,
191 owner,
192 });
193 };
194 DistEntry::Occupied(OccupiedDistEntry {
195 distributions: self,
196 idx,
197 owner,
198 })
199 }
200 fn extend(&mut self, dist: FleetSecretDistribution) {
201 for owner in &dist.owners {
202 self.entry(owner.to_owned()).remove();
203 }
204 self.0.push(dist);
205 }
206 pub fn contains(&self, owner: &str) -> bool {
207 self.0.iter().any(|d| d.owners.contains(owner))
208 }
143}209}
144210
211struct OccupiedDistEntry<'d> {
212 distributions: &'d mut FleetSecretDistributions,
213 idx: usize,
214 owner: String,
215}
216impl<'d> OccupiedDistEntry<'d> {
217 fn remove(self) -> VacantDistEntry<'d> {
218 let dist = &mut self.distributions.0[self.idx];
219 assert!(
220 dist.owners.remove(&self.owner),
221 "entry exists, as we have its reference"
222 );
223 if dist.owners.is_empty() {
224 self.distributions.0.remove(self.idx);
225 }
226 VacantDistEntry {
227 distributions: self.distributions,
228 owner: self.owner,
229 }
230 }
231 fn set(self, secret: FleetSecretData) -> Self {
232 self.remove().set(secret)
233 }
234}
235struct VacantDistEntry<'d> {
236 distributions: &'d mut FleetSecretDistributions,
237 owner: String,
238}
239impl<'d> VacantDistEntry<'d> {
240 fn set(self, secret: FleetSecretData) -> OccupiedDistEntry<'d> {
241 let Self {
242 distributions,
243 owner,
244 } = self;
245 let idx = distributions.0.len();
246 distributions.0.push(FleetSecretDistribution {
247 managed: None,
248 owners: BTreeSet::from_iter([owner.clone()]),
249 secret,
250 });
251 OccupiedDistEntry {
252 distributions,
253 owner,
254 idx,
255 }
256 }
257}
258
259enum DistEntry<'d> {
260 Vacant(VacantDistEntry<'d>),
261 Occupied(OccupiedDistEntry<'d>),
262}
263impl DistEntry<'_> {
264 fn remove(self) -> Self {
265 match self {
266 DistEntry::Vacant(_) => self,
267 DistEntry::Occupied(o) => Self::Vacant(o.remove()),
268 }
269 }
270 fn set(self, secret: FleetSecretData) -> Self {
271 Self::Occupied(match self {
272 DistEntry::Vacant(e) => e.set(secret),
273 DistEntry::Occupied(e) => e.set(secret),
274 })
275 }
276}
277
278impl Serialize for FleetSecretDistributions {
279 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
280 where
281 S: serde::Serializer,
282 {
283 let mut found_hosts = BTreeSet::new();
284 for ele in self.0.iter() {
285 if ele.owners.is_empty() {
286 panic!("consistency: secret distribution has no defined owners");
287 }
288 for ele in ele.owners.iter() {
289 if !found_hosts.insert(ele) {
290 panic!(
291 "consistency: secret distribution contains duplicate entry for the same host",
292 );
293 }
294 }
295 }
296 match self.0.len() {
297 0 => panic!("consistency: empty distributions"),
298 1 => self.0[0].serialize(serializer),
299 _ => self.0.serialize(serializer),
300 }
301 }
302}
303impl<'de> Deserialize<'de> for FleetSecretDistributions {
304 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
305 where
306 D: serde::Deserializer<'de>,
307 {
308 #[derive(Deserialize)]
309 #[serde(untagged)]
310 enum Distributions {
311 One(FleetSecretDistribution),
312 Many(Vec<FleetSecretDistribution>),
313 }
314 let d = Distributions::deserialize(deserializer)?;
315 let ds = match d {
316 Distributions::One(d) => vec![d],
317 Distributions::Many(ds) => ds,
318 };
319 if ds.is_empty() {
320 return Err(de::Error::custom("consistency: empty distributions"));
321 }
322 let mut found_hosts = BTreeSet::new();
323 for ele in ds.iter() {
324 if ele.owners.is_empty() {
325 return Err(de::Error::custom(
326 "consistency: secret distribution has no defined owners",
327 ));
328 }
329 for ele in ele.owners.iter() {
330 if !found_hosts.insert(ele) {
331 return Err(de::Error::custom(
332 "consistency: secret distribution contains duplicate entry for the same host",
333 ));
334 }
335 }
336 }
337 Ok(Self(ds))
338 }
339}
340
341#[derive(Serialize, Deserialize, Default)]
342pub struct FleetSecrets(BTreeMap<String, FleetSecretDistributions>);
343
344impl FleetSecrets {
345 pub fn keys(&self) -> btree_map::Keys<String, FleetSecretDistributions> {
346 self.0.keys()
347 }
348
349 pub fn keys_for_owner(&self, owner: &str) -> impl Iterator<Item = &String> {
350 self.0
351 .iter()
352 .filter(|(_, d)| d.contains(owner))
353 .map(|(n, _)| n)
354 }
355
356 pub fn drop_owner_no_reencrypt(&mut self, secret: &str, owner: &str) -> bool {
145#[derive(Serialize, Deserialize, Clone)]357 let Entry::Occupied(mut dists) = self.0.entry(secret.to_owned()) else {
358 return false;
359 };
146#[serde(rename_all = "camelCase")]360 let DistEntry::Occupied(dist) = dists.get_mut().entry(owner.to_owned()) else {
147#[must_use]361 return false;
362 };
363
364 dist.remove();
365
366 if dists.get().0.is_empty() {
367 dists.remove();
368 };
369
370 true
371 }
148pub struct FleetSharedSecret {372 pub fn set_single_data(&mut self, secret: String, owner: String, data: FleetSecretData) {
149 #[serde(default)]373 let e = self
374 .0
375 .entry(secret.to_owned())
376 .or_insert_with(|| FleetSecretDistributions(Default::default()));
377 e.entry(owner.to_owned()).set(data);
378 }
379 pub fn set_data(&mut self, secret: String, data: FleetSecretDistribution) {
380 match self.0.entry(secret) {
381 Entry::Vacant(e) => {
382 e.insert(FleetSecretDistributions(vec![data]));
383 }
384 Entry::Occupied(mut e) => {
385 let dists = e.get_mut();
386 dists.extend(data)
387 }
388 }
389 }
390 pub fn get_single(&self, secret: &str, owner: &str) -> Option<&FleetSecretDistribution> {
150 #[serde(skip_serializing_if = "Option::is_none")]391 let secret = self.0.get(secret)?;
392 secret.get(owner)
393 }
151 pub managed: Option<bool>,394 pub fn get(&self, secret: &str) -> Option<&FleetSecretDistributions> {
395 self.0.get(secret)
396 }
397
152 pub owners: BTreeSet<String>,398 pub fn contains_for_owner(&self, secret: &str, owner: &str) -> bool {
399 let Some(secret) = self.0.get(secret) else {
400 return false;
401 };
402 secret.contains(owner)
403 }
404 pub fn contains(&self, secret: &str) -> bool {
153 #[serde(flatten)]405 self.0.contains_key(secret)
406 }
154 pub secret: FleetSecretData,407 pub fn remove(&mut self, secret: &str) {
155}408 self.0.remove(secret);
409 }
410
411 fn merge_from_hosts(
412 &mut self,
413 host_secrets: BTreeMap<String, BTreeMap<String, FleetSecretDistribution>>,
414 ) {
415 for (host, host_secrets) in host_secrets {
416 for (secret_name, mut secret_data) in host_secrets {
417 secret_data.owners.insert(host.clone());
418 self.set_data(secret_name, secret_data);
419 }
420 }
421 }
422}
156423
modifiedcrates/fleet-base/src/host.rsdiffbeforeafterboth
2222
23use crate::{23use crate::{
24 command::MyCommand,24 command::MyCommand,
25 fleetdata::{FleetData, FleetHostSecret, FleetSharedSecret},25 fleetdata::{FleetData, FleetSecretData, FleetSecretDistribution, FleetSecretDistributions},
26 secret::{HostSecretDefinition, SharedSecretDefinition},26 secret::{HostSecretDefinition, SharedSecretDefinition},
27};27};
2828
623 let config_field = &self.config_field;623 let config_field = &self.config_field;
624 nix_go!(config_field.sharedSecrets).list_fields()624 nix_go!(config_field.sharedSecrets).list_fields()
625 }625 }
626 /// Shared secrets configured in fleet.nix
627 pub fn list_shared(&self) -> Vec<String> {
628 let data = self.data();
629 data.shared_secrets.keys().cloned().collect()
630 }
631 pub fn has_shared(&self, name: &str) -> bool {626 pub fn has_shared(&self, name: &str) -> bool {
632 let data = self.data();627 let data = self.data();
633 data.shared_secrets.contains_key(name)628 data.secrets.contains(name)
634 }629 }
635 pub fn replace_shared(&self, name: String, shared: FleetSharedSecret) {630 pub fn replace_shared(&self, name: String, shared: FleetSecretDistribution) {
636 let mut data = self.data_mut();631 let mut data = self.data_mut();
637 data.shared_secrets.insert(name.to_owned(), shared);632 data.secrets.set_data(name, shared);
638 }633 }
639 pub fn remove_shared(&self, secret: &str) {634 pub fn remove_shared(&self, secret: &str) {
640 let mut data = self.data_mut();635 let mut data = self.data_mut();
641 data.shared_secrets.remove(secret);636 data.secrets.remove(secret);
642 }637 }
643638
644 pub fn list_secrets(&self, host: &str) -> Vec<String> {639 pub fn list_secrets_for_owner(&self, host: &str) -> Vec<String> {
645 let data = self.data();640 let data = self.data_mut();
646 let mut out = data641 data.secrets.keys_for_owner(host).cloned().collect()
647 .host_secrets
648 .get(host)
649 .map(|s| s.keys().cloned().collect::<Vec<String>>())
650 .unwrap_or_default();
651
652 for (name, shared) in data.shared_secrets.iter() {
653 if shared.owners.contains(host) {
654 out.push(name.clone());
655 }
656 }
657
658 out
659 }642 }
643 pub fn list_secrets(&self) -> Vec<String> {
644 let data = self.data_mut();
645 data.secrets.keys().cloned().collect()
646 }
660647
661 pub fn has_secret(&self, host: &str, secret: &str) -> bool {648 pub fn has_secret(&self, host: &str, secret: &str) -> bool {
662 let data = self.data();649 let data = self.data();
663 let Some(host_secrets) = data.host_secrets.get(host) else {650 data.secrets.contains_for_owner(secret, host)
664 return false;
665 };
666 host_secrets.contains_key(secret)
667 }651 }
668 pub fn insert_secret(&self, host: &str, secret: String, value: FleetHostSecret) {652 pub fn insert_secret(&self, host: String, secret: String, value: FleetSecretData) {
669 let mut data = self.data_mut();653 let mut data = self.data_mut();
670 let host_secrets = data.host_secrets.entry(host.to_owned()).or_default();
671 host_secrets.insert(secret, value);654 data.secrets.set_single_data(secret, host, value);
672 }655 }
673 pub fn remove_secret(&self, host: &str, secret: &str) {656 pub fn remove_secret(&self, host: &str, secret: &str) {
674 let mut data = self.data_mut();657 let mut data = self.data_mut();
675 let host_secrets = data.host_secrets.entry(host.to_owned()).or_default();658 data.secrets.drop_owner_no_reencrypt(secret, host);
676 host_secrets.remove(secret);
677 }659 }
678660
679 pub fn host_secret(&self, host: &str, secret: &str) -> Result<FleetHostSecret> {661 pub fn host_secret(&self, host: &str, secret: &str) -> Option<FleetSecretDistribution> {
680 let data = self.data();662 let data = self.data();
681 if let Some(host_secrets) = data.host_secrets.get(host) {663 data.secrets.get_single(secret, host).cloned()
682 if let Some(secret) = host_secrets.get(secret) {
683 return Ok(secret.clone());
684 }
685 };
686 let Some(shared) = data.shared_secrets.get(secret) else {
687 bail!("machine {host} has no secret {secret}");
688 };
689 if !shared.owners.contains(host) {
690 bail!("shared secret {secret} is not owned by {host}");
691 };
692 Ok(FleetHostSecret {
693 managed: shared.managed,
694 secret: shared.secret.clone(),
695 })
696 }664 }
697 pub fn shared_secret(&self, secret: &str) -> Result<Option<FleetSharedSecret>> {665 pub fn shared_secret(&self, secret: &str) -> Option<FleetSecretDistributions> {
698 let data = self.data();666 let data = self.data();
699 Ok(data.shared_secrets.get(secret).cloned())667 data.secrets.get(secret).cloned()
700 }668 }
701 pub fn shared_secret_definition(&self, secret: &str) -> Result<SharedSecretDefinition> {669 pub fn shared_secret_definition(&self, secret: &str) -> Result<SharedSecretDefinition> {
702 let config_field = &self.config_field;670 let config_field = &self.config_field;
modifiedcrates/fleet-base/src/opts.rsdiffbeforeafterboth
211 }211 }
212 let bytes =212 let bytes =
213 std::fs::read_to_string(&fleet_data_path).context("reading fleet state (fleet.nix)")?;213 std::fs::read_to_string(&fleet_data_path).context("reading fleet state (fleet.nix)")?;
214 let data: Mutex<FleetData> = nixlike::parse_str(&bytes)?;214 let data = Mutex::new(FleetData::from_str(&bytes)?);
215215
216 let mut fetch_settings = FetchSettings::new();216 let mut fetch_settings = FetchSettings::new();
217 fetch_settings.set(c"warn-dirty", c"false");217 fetch_settings.set(c"warn-dirty", c"false");
modifiedflake.lockdiffbeforeafterboth
2 "nodes": {2 "nodes": {
3 "crane": {3 "crane": {
4 "locked": {4 "locked": {
5 "lastModified": 1766181779,5 "lastModified": 1767461147,
6 "owner": "ipetkov",6 "owner": "ipetkov",
7 "repo": "crane",7 "repo": "crane",
8 "rev": "0263f510ba38bee5b7f817498066adaad694e50b",8 "rev": "7d59256814085fd9666a2ae3e774dc5ee216b630",
9 "type": "github"9 "type": "github"
10 },10 },
11 "original": {11 "original": {
37 ]37 ]
38 },38 },
39 "locked": {39 "locked": {
40 "lastModified": 1765835352,40 "lastModified": 1767609335,
41 "owner": "hercules-ci",41 "owner": "hercules-ci",
42 "repo": "flake-parts",42 "repo": "flake-parts",
43 "rev": "a34fae9c08a15ad73f295041fec82323541400a9",43 "rev": "250481aafeb741edfe23d29195671c19b36b6dca",
44 "type": "github"44 "type": "github"
45 },45 },
46 "original": {46 "original": {
126 },126 },
127 "nixpkgs": {127 "nixpkgs": {
128 "locked": {128 "locked": {
129 "lastModified": 1766181714,129 "lastModified": 1767657734,
130 "owner": "nixos",130 "owner": "nixos",
131 "repo": "nixpkgs",131 "repo": "nixpkgs",
132 "rev": "ff2da5fee8b3248cac330f14eac98228620beab0",132 "rev": "d4ccebf51ee4dbeb9df364dce1fe9848635c1258",
133 "type": "github"133 "type": "github"
134 },134 },
135 "original": {135 "original": {
190 ]190 ]
191 },191 },
192 "locked": {192 "locked": {
193 "lastModified": 1766112155,193 "lastModified": 1767667566,
194 "owner": "oxalica",194 "owner": "oxalica",
195 "repo": "rust-overlay",195 "repo": "rust-overlay",
196 "rev": "2a6db3fc1c27ae77f9caa553d7609b223cb770b5",196 "rev": "056ce5b125ab32ffe78c7d3e394d9da44733c95e",
197 "type": "github"197 "type": "github"
198 },198 },
199 "original": {199 "original": {
223 ]223 ]
224 },224 },
225 "locked": {225 "locked": {
226 "lastModified": 1766000401,226 "lastModified": 1767468822,
227 "owner": "numtide",227 "owner": "numtide",
228 "repo": "treefmt-nix",228 "repo": "treefmt-nix",
229 "rev": "42d96e75aa56a3f70cab7e7dc4a32868db28e8fd",229 "rev": "d56486eb9493ad9c4777c65932618e9c2d0468fc",
230 "type": "github"230 "type": "github"
231 },231 },
232 "original": {232 "original": {
modifiedflake.nixdiffbeforeafterboth
128 overlays = [128 overlays = [
129 (inputs.rust-overlay.overlays.default)129 (inputs.rust-overlay.overlays.default)
130 (final: prev: {130 (final: prev: {
131 boehmgc = prev.boehmgc.overrideAttrs (prevAttrs: {
132 configureFlags = prevAttrs.configureFlags ++ [
133 "--enable-gc-assertions"
134 ];
135 });
136 # Libsecret is stupidly huge131 # Libsecret is stupidly huge
137 # https://github.com/oxalica/rust-overlay/issues/211132 # https://github.com/oxalica/rust-overlay/issues/211
138 libsecret = final.stdenv.mkDerivation {133 libsecret = final.stdenv.mkDerivation {
modifiedmodules/secrets-data.nixdiffbeforeafterboth
1{1{
2 lib,2 lib,
3 fleetLib,3 fleetLib,
4 config,
5 ...4 ...
6}:5}:
7let6let
16 bool15 bool
17 unspecified16 unspecified
18 ;17 ;
19 inherit (lib.attrsets)
20 mapAttrsToList
21 mapAttrs
22 filterAttrs
23 genAttrs
24 ;
25 inherit (lib.lists) sort unique concatLists;
26 inherit (lib.strings) toJSON;
2718
28 secretDataValue = {19 secretDataValue = {
29 options = {20 options = {
71 default = null;62 default = null;
72 };63 };
73 };64 };
74 config = { };
75 };65 };
7666
77 hostSecretData = {
78 freeformType = attrsOf (submodule secretDataValue);
79 options = {
80 createdAt = mkOption {
81 type = str;
82 description = "Timestamp of secret generation/last rotation.";
83 default = null;
84 };
85 expiresAt = mkOption {
86 type = nullOr str;
87 description = "Expiration timestamp triggering mandatory secret rotation.";
88 default = null;
89 };
90 shared = mkOption {
91 type = bool;
92 description = "Indicates if secret is a shared secret, so other hosts might have the same piece of secret data.";
93 default = false;
94 };
95 generationData = mkOption {
96 type = unspecified;
97 description = "Contextual metadata associated with secret part.";
98 default = null;
99 };
100 };
101 config = { };
102 };
103 managerKey = {67 managerKey = {
104 options = {68 options = {
105 name = mkOption {69 name = mkOption {
121 managerKeys = mkOption {85 managerKeys = mkOption {
122 type = listOf (submodule managerKey);86 type = listOf (submodule managerKey);
123 };87 };
124 sharedSecrets = mkOption {88 secrets = mkOption {
125 type = attrsOf (submodule sharedSecretData);89 type = attrsOf (listOf submodule sharedSecretData);
126 default = { };90 default = { };
127 description = "Shared secret data.";91 description = "Shared secret data.";
128 };92 };
129 hostSecrets = mkOption {
130 type = attrsOf (attrsOf (submodule hostSecretData));
131 default = { };
132 description = "Host-specific secrets.";
133 internal = true;
134 };
135 };93 };
136 config.hostSecrets =
137 let
138 hostsWithSharedSecrets = unique (
139 concatLists (mapAttrsToList (_: s: s.owners) config.sharedSecrets)
140 );
141 secretsHavingHost = host: filterAttrs (_: secret: lib.elem host secret.owners) config.sharedSecrets;
142 toHostSecret = _: secret: (removeAttrs secret [ "owners" ]) // { shared = true; };
143 in
144 genAttrs hostsWithSharedSecrets (host: mapAttrs toHostSecret (secretsHavingHost host));
145 });94 });
146 config = {
147 assertions =
148 (mapAttrsToList (name: secret: {
149 assertion =
150 secret.expectedOwners == null
151 ||
152 sort (a: b: a < b) (config.data.sharedSecrets.${name} or { owners = [ ]; }).owners
153 == sort (a: b: a < b) secret.expectedOwners;
154 message = "Shared secret ${name} is expected to be encrypted for ${toJSON secret.expectedOwners}, but it is encrypted for ${
155 toJSON (config.data.sharedSecrets.${name} or { owners = [ ]; }).owners
156 }. Run fleet secrets regenerate to fix";
157 }) config.sharedSecrets)
158
159 ++ (mapAttrsToList (name: secret: {
160 # TODO: Same assertion should be in host secrets
161 assertion =
162 (config.data.sharedSecrets.${name} or { generationData = null; }).generationData
163 == secret.expectedGenerationData;
164 message = "Shared secret ${name} has unexpected generation data ${toJSON secret.expectedGenerationData} != ${
165 toJSON (config.data.sharedSecrets.${name} or { generationData = null; }).generationData
166 }. Run fleet secrets regenerate to fix";
167 }) config.sharedSecrets);
168 };
169}95}
17096
modifiedmodules/secrets.nixdiffbeforeafterboth
1{1{
2 lib,2 lib,
3 config,
4 ...3 ...
5}:4}:
6let5let
18 uniq17 uniq
19 ;18 ;
20 inherit (lib.strings) concatStringsSep;19 inherit (lib.strings) concatStringsSep;
21 inherit (lib.attrsets) mapAttrs;
2220
23 sharedSecret =21 sharedSecret =
24 { config, ... }:22 { config, ... }:
54 Set to false if host permissions are revoked through alternative mechanisms like firewall rules.52 Set to false if host permissions are revoked through alternative mechanisms like firewall rules.
55 '';53 '';
56 };54 };
55 allowDifferent = mkOption {
56 type = bool;
57 description = ''
58 When adding owner, do not update secret value for other owners, instead creating a new distribution
59 '';
60 };
57 generator = mkOption {61 generator = mkOption {
58 type = uniq (nullOr (functionTo package));62 type = uniq (nullOr (functionTo package));
59 description = ''63 description = ''
84in88in
85{89{
86 options = {90 options = {
87 sharedSecrets = mkOption {91 secrets = mkOption {
88 type = attrsOf (submodule sharedSecret);92 type = attrsOf (submodule sharedSecret);
89 default = { };93 default = { };
90 description = "Collection of secrets shared across multiple hosts with configurable ownership";94 description = "Collection of secrets shared across multiple hosts with configurable ownership";
91 };95 };
92 };96 };
93 config = {97 config = {
94 hosts = mapAttrs (
95 _: secretMap:
96 let
97 partsOf =
98 s:
99 removeAttrs s [
100 "createdAt"
101 "expiresAt"
102 "generationData"
103 ];
104
105 in
106 {
107 nixos.data.secrets = mapAttrs (_: s: partsOf s) secretMap;
108 # nixos.secrets = mapAttrs (
109 # _: s: mapAttrs (_: _: {}) (partsOf s)
110 # ) secretMap;
111 }
112 ) config.data.hostSecrets;
113 nixpkgs.overlays = [98 nixpkgs.overlays = [
114 (final: prev: {99 (final: prev: {
115 mkSecretGenerators =100 mkSecretGenerators =