Skip to content

Commit a43bc97

Browse files
nightkrsbernauer
andauthored
Rotate AutoTLS CAs (#350)
* Fix doccomment to be more neutral towards self-signed CAs * Turn tls backend into a directory module * Factor out CA management * Rotate CA if about to expire * Reorganize and document/test * Rename ca::Manager::all_cas -> trust_roots * Refactor internal naming * Sort CAs by age to avoid spurious writes * Docs * Changelog * Update CRD * Reshuffle logging * rustfmt * Fix broken doclink * Update rust/operator-binary/src/utils.rs Co-authored-by: Sebastian Bernauer <[email protected]> * Enforce secret reference correctness in CRD * Check for time overflow * Make cargo-deny happy * Add note about cleaning up CAs --------- Co-authored-by: Sebastian Bernauer <[email protected]>
1 parent e1b8f34 commit a43bc97

File tree

21 files changed

+739
-308
lines changed

21 files changed

+739
-308
lines changed

CHANGELOG.md

+4-1
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,13 @@ All notable changes to this project will be documented in this file.
1010

1111
## Changed
1212

13-
- Use new annotation builder ([#341])
13+
- Use new annotation builder ([#341]).
14+
- `autoTLS` certificate authorities will now be rotated regularly ([#350]).
15+
- [BREAKING] This changes the format of the CA secrets. Old secrets will be migrated automatically, but manual intervention will be required to downgrade back to 23.11.x.
1416

1517
[#333]: https://github.com/stackabletech/secret-operator/pull/333
1618
[#341]: https://github.com/stackabletech/secret-operator/pull/341
19+
[#350]: https://github.com/stackabletech/secret-operator/pull/350
1720

1821
## [23.11.0] - 2023-11-24
1922

Cargo.lock

+10
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[workspace]
2-
members = ["rust/operator-binary", "rust/krb5", "rust/krb5-provision-keytab", "rust/krb5-sys"]
2+
members = ["rust/*"]
33
default-members = ["rust/operator-binary"]
44
resolver = "2"
55

deploy/helm/secret-operator/crds/crds.yaml

+21-9
Original file line numberDiff line numberDiff line change
@@ -45,17 +45,20 @@ spec:
4545
properties:
4646
autoGenerate:
4747
default: false
48-
description: Whether a new certificate authority should be generated if it does not already exist.
48+
description: Whether the certificate authority should be managed by Secret Operator, including being generated if it does not already exist.
4949
type: boolean
5050
secret:
5151
description: Reference (name and namespace) to a Kubernetes Secret object where the CA certificate and key is stored in the keys `ca.crt` and `ca.key` respectively.
5252
properties:
5353
name:
54-
description: name is unique within a namespace to reference a secret resource.
54+
description: Name of the Secret being referred to.
5555
type: string
5656
namespace:
57-
description: namespace defines the space within which the secret name must be unique.
57+
description: Namespace of the Secret being referred to.
5858
type: string
59+
required:
60+
- name
61+
- namespace
5962
type: object
6063
required:
6164
- secret
@@ -109,21 +112,27 @@ spec:
109112
description: Reference (name and namespace) to a Kubernetes Secret object containing the TLS CA (in `ca.crt`) that the LDAP server’s certificate should be authenticated against.
110113
properties:
111114
name:
112-
description: name is unique within a namespace to reference a secret resource.
115+
description: Name of the Secret being referred to.
113116
type: string
114117
namespace:
115-
description: namespace defines the space within which the secret name must be unique.
118+
description: Namespace of the Secret being referred to.
116119
type: string
120+
required:
121+
- name
122+
- namespace
117123
type: object
118124
passwordCacheSecret:
119125
description: Reference (name and namespace) to a Kubernetes Secret object where workload passwords will be stored. This must not be accessible to end users.
120126
properties:
121127
name:
122-
description: name is unique within a namespace to reference a secret resource.
128+
description: Name of the Secret being referred to.
123129
type: string
124130
namespace:
125-
description: namespace defines the space within which the secret name must be unique.
131+
description: Namespace of the Secret being referred to.
126132
type: string
133+
required:
134+
- name
135+
- namespace
127136
type: object
128137
schemaDistinguishedName:
129138
description: The root Distinguished Name (DN) for AD-managed schemas, typically `CN=Schema,CN=Configuration,{domain_dn}`.
@@ -152,11 +161,14 @@ spec:
152161
description: Reference (`name` and `namespace`) to a K8s Secret object where a keytab with administrative privileges is stored in the key `keytab`.
153162
properties:
154163
name:
155-
description: name is unique within a namespace to reference a secret resource.
164+
description: Name of the Secret being referred to.
156165
type: string
157166
namespace:
158-
description: namespace defines the space within which the secret name must be unique.
167+
description: Namespace of the Secret being referred to.
159168
type: string
169+
required:
170+
- name
171+
- namespace
160172
type: object
161173
adminPrincipal:
162174
description: The admin principal.

deploy/helm/secret-operator/templates/roles.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ rules:
1515
- watch
1616
- create
1717
- patch
18+
- update
1819
- apiGroups:
1920
- ""
2021
resources:

docs/modules/secret-operator/pages/secretclass.adoc

+13-2
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,18 @@ We have spent a considerate amount of time thinking about this issue and decided
5656
Most of our product operators will not set any specific certificate lifetime, so the default applies.
5757
In case an operator sets a higher lifetime, a tracking issue must be created to document and track the steps to reduce the certificate lifetime.
5858

59-
Users can use podOverrides to extend the certificate lifetime by adding volume annotations. We might add native support to customize certificate lifetimes in the future by using the product CRDs.
59+
Users can use podOverrides to extend the certificate lifetime by adding volume annotations. We might add native support for customizing certificate lifetimes in the future to the Stacklet CRDs.
60+
61+
==== Certificate Authority rotation
62+
63+
Certificate authorities also have a limited lifetime, and need to be rotated before they expire to avoid cluster disruption.
64+
65+
If configured to provision its own CA (`autoTls.ca.autoGenerate`), the Secret Operator will create CA certificates that are valid for 2 years,
66+
and initiate rotation when there is less than 1 year remaining. If configured _not_ to provision its own CA, a warning will be issued when there is less than 1 year remaining.
67+
68+
To avoid disruption and let the new CA propagate through the cluster, the Secret Operator will prefer using the oldest CA that will last for the entire lifetime of the issued certificate.
69+
70+
Expired certificates will currently not be deleted automatically, and should be cleaned up manually.
6071

6172
==== Reference
6273

@@ -77,7 +88,7 @@ spec:
7788
`autoTls.ca`:: Configures the certificate authority used to issue `Pod` certificates.
7889
`autoTls.ca.secret`:: Reference (`name` and `namespace`) to a K8s `Secret` object where the CA certificate and key is stored in the keys `ca.crt`
7990
and `ca.key` respectively.
80-
`autoTls.ca.autoGenerate`:: Whether the certificate authority should be provisioned if it can not be found.
91+
`autoTls.ca.autoGenerate`:: Whether the certificate authority should be provisioned and managed by the Secret Operator.
8192
`autoTls.maxCertificateLifetime`:: Maximum lifetime the created certificates are allowed to have. In case consumers request a longer lifetime than allowed by this setting, the lifetime will be the minimum of both.
8293

8394
[#backend-kerberoskeytab]

rust/crd-utils/Cargo.toml

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[package]
2+
name = "stackable-secret-operator-crd-utils"
3+
version.workspace = true
4+
authors.workspace = true
5+
license.workspace = true
6+
edition.workspace = true
7+
repository.workspace = true
8+
publish = false
9+
10+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
11+
12+
[dependencies]
13+
serde.workspace = true
14+
stackable-operator.workspace = true

rust/crd-utils/src/lib.rs

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
//! CRD types that are shared between secret-operator components, but aren't clearly owned by one of them.
2+
3+
use std::fmt::Display;
4+
5+
use serde::{Deserialize, Serialize};
6+
use stackable_operator::{
7+
k8s_openapi::api::core::v1::Secret,
8+
kube::runtime::reflector::ObjectRef,
9+
schemars::{self, JsonSchema},
10+
};
11+
12+
// Redefine SecretReference instead of reusing k8s-openapi's, in order to make name/namespace mandatory.
13+
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
14+
#[serde(rename_all = "camelCase")]
15+
pub struct SecretReference {
16+
/// Namespace of the Secret being referred to.
17+
pub namespace: String,
18+
/// Name of the Secret being referred to.
19+
pub name: String,
20+
}
21+
22+
// Use ObjectRef for logging/errors
23+
impl Display for SecretReference {
24+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25+
ObjectRef::<Secret>::from(self).fmt(f)
26+
}
27+
}
28+
impl From<SecretReference> for ObjectRef<Secret> {
29+
fn from(val: SecretReference) -> Self {
30+
ObjectRef::<Secret>::from(&val)
31+
}
32+
}
33+
impl From<&SecretReference> for ObjectRef<Secret> {
34+
fn from(val: &SecretReference) -> Self {
35+
ObjectRef::<Secret>::new(&val.name).within(&val.namespace)
36+
}
37+
}

rust/krb5-provision-keytab/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ publish = false
1010

1111
[dependencies]
1212
krb5 = { path = "../krb5" }
13+
stackable-secret-operator-crd-utils = { path = "../crd-utils" }
1314

1415
byteorder.workspace = true
1516
futures.workspace = true

rust/krb5-provision-keytab/src/active_directory.rs

+11-28
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,17 @@ use krb5::{Keyblock, Keytab, KrbContext, Principal, PrincipalUnparseOptions};
55
use ldap3::{Ldap, LdapConnAsync, LdapConnSettings};
66
use rand::{seq::SliceRandom, thread_rng, CryptoRng};
77
use snafu::{OptionExt, ResultExt, Snafu};
8-
use stackable_operator::k8s_openapi::api::core::v1::{Secret, SecretReference};
8+
use stackable_operator::{k8s_openapi::api::core::v1::Secret, kube::runtime::reflector::ObjectRef};
9+
use stackable_secret_operator_crd_utils::SecretReference;
910

10-
use crate::{
11-
credential_cache::{self, CredentialCache},
12-
secret_ref::{FullSecretRef, IncompleteSecretRef},
13-
};
11+
use crate::credential_cache::{self, CredentialCache};
1412

1513
#[derive(Debug, Snafu)]
1614
pub enum Error {
17-
#[snafu(display("LDAP TLS CA reference is invalid"))]
18-
LdapTlsCaReferenceInvalid { source: IncompleteSecretRef },
19-
2015
#[snafu(display("failed to retrieve LDAP TLS CA {ca_ref}"))]
2116
GetLdapTlsCa {
2217
source: stackable_operator::error::Error,
23-
ca_ref: FullSecretRef,
18+
ca_ref: ObjectRef<Secret>,
2419
},
2520

2621
#[snafu(display("LDAP TLS CA secret is missing required key {key}"))]
@@ -29,9 +24,6 @@ pub enum Error {
2924
#[snafu(display("failed to parse LDAP TLS CA"))]
3025
ParseLdapTlsCa { source: native_tls::Error },
3126

32-
#[snafu(display("password cache reference is invalid"))]
33-
PasswordCacheReferenceInvalid { source: IncompleteSecretRef },
34-
3527
#[snafu(display("password cache error"))]
3628
PasswordCache { source: credential_cache::Error },
3729

@@ -55,13 +47,13 @@ pub enum Error {
5547
#[snafu(display("failed to get password cache {password_cache_ref}"))]
5648
GetPasswordCache {
5749
source: stackable_operator::error::Error,
58-
password_cache_ref: FullSecretRef,
50+
password_cache_ref: ObjectRef<Secret>,
5951
},
6052

6153
#[snafu(display("failed to update password cache {password_cache_ref}"))]
6254
UpdatePasswordCache {
6355
source: stackable_operator::error::Error,
64-
password_cache_ref: FullSecretRef,
56+
password_cache_ref: ObjectRef<Secret>,
6557
},
6658

6759
#[snafu(display("failed to create LDAP user"))]
@@ -72,7 +64,7 @@ pub enum Error {
7264
))]
7365
CreateLdapUserConflict {
7466
source: ldap3::LdapError,
75-
password_cache_ref: FullSecretRef,
67+
password_cache_ref: ObjectRef<Secret>,
7668
},
7769

7870
#[snafu(display("failed to decode generated password"))]
@@ -121,15 +113,9 @@ impl<'a> AdAdmin<'a> {
121113
ldap.sasl_gssapi_bind(ldap_server)
122114
.await
123115
.context(LdapAuthnSnafu)?;
124-
let password_cache = CredentialCache::new(
125-
"AD passwords",
126-
kube,
127-
password_cache_secret
128-
.try_into()
129-
.context(PasswordCacheReferenceInvalidSnafu)?,
130-
)
131-
.await
132-
.context(PasswordCacheSnafu)?;
116+
let password_cache = CredentialCache::new("AD passwords", kube, password_cache_secret)
117+
.await
118+
.context(PasswordCacheSnafu)?;
133119
Ok(Self {
134120
ldap,
135121
krb,
@@ -190,9 +176,6 @@ async fn get_ldap_ca_certificate(
190176
kube: &stackable_operator::client::Client,
191177
ca_secret_ref: SecretReference,
192178
) -> Result<native_tls::Certificate> {
193-
let ca_secret_ref: FullSecretRef = ca_secret_ref
194-
.try_into()
195-
.context(LdapTlsCaReferenceInvalidSnafu)?;
196179
let ca_secret = kube
197180
.get::<Secret>(&ca_secret_ref.name, &ca_secret_ref.namespace)
198181
.await
@@ -241,7 +224,7 @@ async fn create_ad_user(
241224
password: &str,
242225
user_dn_base: &str,
243226
schema_dn_base: &str,
244-
password_cache_ref: FullSecretRef,
227+
password_cache_ref: SecretReference,
245228
) -> Result<()> {
246229
// Flags are a subset of https://learn.microsoft.com/en-us/troubleshoot/windows-server/identity/useraccountcontrol-manipulate-account-properties
247230
const AD_UAC_NORMAL_ACCOUNT: u32 = 0x0200;

rust/krb5-provision-keytab/src/credential_cache.rs

+11-9
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,46 @@
11
use futures::{TryFuture, TryFutureExt};
22
use snafu::{OptionExt, ResultExt, Snafu};
3-
use stackable_operator::k8s_openapi::{api::core::v1::Secret, ByteString};
4-
5-
use crate::secret_ref::FullSecretRef;
3+
use stackable_operator::{
4+
k8s_openapi::{api::core::v1::Secret, ByteString},
5+
kube::runtime::reflector::ObjectRef,
6+
};
7+
use stackable_secret_operator_crd_utils::SecretReference;
68

79
#[derive(Debug, Snafu)]
810
pub enum Error {
911
#[snafu(display("failed to load initial cache from {cache_ref}"))]
1012
GetInitialCache {
1113
source: stackable_operator::error::Error,
12-
cache_ref: FullSecretRef,
14+
cache_ref: ObjectRef<Secret>,
1315
},
1416

1517
#[snafu(display("failed to save credential {key} to {cache_ref}"))]
1618
SaveToCache {
1719
source: stackable_operator::error::Error,
1820
key: String,
19-
cache_ref: FullSecretRef,
21+
cache_ref: ObjectRef<Secret>,
2022
},
2123

2224
#[snafu(display("newly saved credential {key} was not found in {cache_ref}"))]
2325
SavedKeyNotFound {
2426
key: String,
25-
cache_ref: FullSecretRef,
27+
cache_ref: ObjectRef<Secret>,
2628
},
2729
}
2830
type Result<T, E = Error> = std::result::Result<T, E>;
2931

3032
pub struct CredentialCache {
3133
name: &'static str,
3234
kube: stackable_operator::client::Client,
33-
cache_ref: FullSecretRef,
35+
cache_ref: SecretReference,
3436
current_state: Secret,
3537
}
3638
impl CredentialCache {
3739
#[tracing::instrument(skip(kube))]
3840
pub async fn new(
3941
name: &'static str,
4042
kube: stackable_operator::client::Client,
41-
cache_ref: FullSecretRef,
43+
cache_ref: SecretReference,
4244
) -> Result<Self> {
4345
Ok(Self {
4446
name,
@@ -127,5 +129,5 @@ impl CredentialCache {
127129

128130
/// Information that may be useful for generating error messages in get_or_insert handlers
129131
pub struct Ctx {
130-
pub cache_ref: FullSecretRef,
132+
pub cache_ref: SecretReference,
131133
}

0 commit comments

Comments
 (0)