/proc/self/cwd/source/common/secret/sds_api.cc
Line | Count | Source (jump to first uncovered line) |
1 | | #include "source/common/secret/sds_api.h" |
2 | | |
3 | | #include "envoy/config/core/v3/config_source.pb.h" |
4 | | #include "envoy/extensions/transport_sockets/tls/v3/cert.pb.h" |
5 | | #include "envoy/service/discovery/v3/discovery.pb.h" |
6 | | |
7 | | #include "source/common/common/assert.h" |
8 | | #include "source/common/config/api_version.h" |
9 | | #include "source/common/grpc/common.h" |
10 | | #include "source/common/protobuf/utility.h" |
11 | | |
12 | | namespace Envoy { |
13 | | namespace Secret { |
14 | | |
15 | 0 | SdsApiStats SdsApi::generateStats(Stats::Scope& scope) { |
16 | 0 | return {ALL_SDS_API_STATS(POOL_COUNTER(scope))}; |
17 | 0 | } |
18 | | |
19 | | SdsApi::SdsApi(envoy::config::core::v3::ConfigSource sds_config, absl::string_view sds_config_name, |
20 | | Config::SubscriptionFactory& subscription_factory, TimeSource& time_source, |
21 | | ProtobufMessage::ValidationVisitor& validation_visitor, Stats::Store& stats, |
22 | | std::function<void()> destructor_cb, Event::Dispatcher& dispatcher, Api::Api& api) |
23 | | : Envoy::Config::SubscriptionBase<envoy::extensions::transport_sockets::tls::v3::Secret>( |
24 | | validation_visitor, "name"), |
25 | 0 | init_target_(fmt::format("SdsApi {}", sds_config_name), [this] { initialize(); }), |
26 | | dispatcher_(dispatcher), api_(api), |
27 | | scope_(stats.createScope(absl::StrCat("sds.", sds_config_name, "."))), |
28 | | sds_api_stats_(generateStats(*scope_)), sds_config_(std::move(sds_config)), |
29 | | sds_config_name_(sds_config_name), clean_up_(std::move(destructor_cb)), |
30 | | subscription_factory_(subscription_factory), |
31 | | time_source_(time_source), secret_data_{sds_config_name_, "uninitialized", |
32 | 0 | time_source_.systemTime()} { |
33 | 0 | const auto resource_name = getResourceName(); |
34 | | // This has to happen here (rather than in initialize()) as it can throw exceptions. |
35 | 0 | subscription_ = THROW_OR_RETURN_VALUE( |
36 | 0 | subscription_factory_.subscriptionFromConfigSource( |
37 | 0 | sds_config_, Grpc::Common::typeUrl(resource_name), *scope_, *this, resource_decoder_, {}), |
38 | 0 | Config::SubscriptionPtr); |
39 | 0 | } |
40 | | |
41 | | void SdsApi::resolveDataSource(const FileContentMap& files, |
42 | 0 | envoy::config::core::v3::DataSource& data_source) { |
43 | 0 | if (data_source.specifier_case() == |
44 | 0 | envoy::config::core::v3::DataSource::SpecifierCase::kFilename) { |
45 | 0 | const std::string& content = files.at(data_source.filename()); |
46 | 0 | data_source.set_inline_bytes(content); |
47 | 0 | } |
48 | 0 | } |
49 | | |
50 | 0 | void SdsApi::onWatchUpdate() { |
51 | | // Filesystem reads and update callbacks can fail if the key material is missing or bad. We're not |
52 | | // under an onConfigUpdate() context, so we need to catch these cases explicitly here. |
53 | 0 | TRY_ASSERT_MAIN_THREAD { |
54 | | // Obtain a stable set of files. If a rotation happens while we're reading, |
55 | | // then we need to try again. |
56 | 0 | uint64_t prev_hash = 0; |
57 | 0 | FileContentMap files = loadFiles(); |
58 | 0 | uint64_t next_hash = getHashForFiles(files); |
59 | 0 | const uint64_t MaxBoundedRetries = 5; |
60 | 0 | for (uint64_t bounded_retries = MaxBoundedRetries; |
61 | 0 | next_hash != prev_hash && bounded_retries > 0; --bounded_retries) { |
62 | 0 | files = loadFiles(); |
63 | 0 | prev_hash = next_hash; |
64 | 0 | next_hash = getHashForFiles(files); |
65 | 0 | } |
66 | 0 | if (next_hash != prev_hash) { |
67 | 0 | ENVOY_LOG_MISC( |
68 | 0 | warn, "Unable to atomically refresh secrets due to > {} non-atomic rotations observed", |
69 | 0 | MaxBoundedRetries); |
70 | 0 | } |
71 | 0 | const uint64_t new_hash = next_hash; |
72 | 0 | if (new_hash != files_hash_) { |
73 | 0 | resolveSecret(files); |
74 | 0 | THROW_IF_NOT_OK(update_callback_manager_.runCallbacks()); |
75 | 0 | files_hash_ = new_hash; |
76 | 0 | } |
77 | 0 | } |
78 | 0 | END_TRY |
79 | 0 | CATCH(const EnvoyException& e, { |
80 | 0 | ENVOY_LOG_MISC(warn, fmt::format("Failed to reload certificates: {}", e.what())); |
81 | 0 | sds_api_stats_.key_rotation_failed_.inc(); |
82 | 0 | }); |
83 | 0 | } |
84 | | |
85 | | absl::Status SdsApi::onConfigUpdate(const std::vector<Config::DecodedResourceRef>& resources, |
86 | 0 | const std::string& version_info) { |
87 | 0 | const absl::Status status = validateUpdateSize(resources.size(), 0); |
88 | 0 | if (!status.ok()) { |
89 | 0 | return status; |
90 | 0 | } |
91 | 0 | const auto& secret = dynamic_cast<const envoy::extensions::transport_sockets::tls::v3::Secret&>( |
92 | 0 | resources[0].get().resource()); |
93 | |
|
94 | 0 | if (secret.name() != sds_config_name_) { |
95 | 0 | return absl::InvalidArgumentError( |
96 | 0 | fmt::format("Unexpected SDS secret (expecting {}): {}", sds_config_name_, secret.name())); |
97 | 0 | } |
98 | | |
99 | 0 | const uint64_t new_hash = MessageUtil::hash(secret); |
100 | |
|
101 | 0 | if (new_hash != secret_hash_) { |
102 | 0 | validateConfig(secret); |
103 | 0 | secret_hash_ = new_hash; |
104 | 0 | setSecret(secret); |
105 | 0 | const auto files = loadFiles(); |
106 | 0 | files_hash_ = getHashForFiles(files); |
107 | 0 | resolveSecret(files); |
108 | 0 | THROW_IF_NOT_OK(update_callback_manager_.runCallbacks()); |
109 | | |
110 | 0 | auto* watched_directory = getWatchedDirectory(); |
111 | | // Either we have a watched path and can defer the watch monitoring to a |
112 | | // WatchedDirectory object, or we need to implement per-file watches in the else |
113 | | // clause. |
114 | 0 | if (watched_directory != nullptr) { |
115 | 0 | watched_directory->setCallback([this]() { |
116 | 0 | onWatchUpdate(); |
117 | 0 | return absl::OkStatus(); |
118 | 0 | }); |
119 | 0 | } else { |
120 | | // List DataSources that refer to files |
121 | 0 | auto files = getDataSourceFilenames(); |
122 | 0 | if (!files.empty()) { |
123 | | // Create new watch, also destroys the old watch if any. |
124 | 0 | watcher_ = dispatcher_.createFilesystemWatcher(); |
125 | 0 | for (auto const& filename : files) { |
126 | | // Watch for directory instead of file. This allows users to do atomic renames |
127 | | // on directory level (e.g. Kubernetes secret update). |
128 | 0 | const auto result_or_error = api_.fileSystem().splitPathFromFilename(filename); |
129 | 0 | RETURN_IF_NOT_OK_REF(result_or_error.status()); |
130 | 0 | RETURN_IF_NOT_OK(watcher_->addWatch(absl::StrCat(result_or_error.value().directory_, "/"), |
131 | 0 | Filesystem::Watcher::Events::MovedTo, |
132 | 0 | [this](uint32_t) { |
133 | 0 | onWatchUpdate(); |
134 | 0 | return absl::OkStatus(); |
135 | 0 | })); |
136 | 0 | } |
137 | 0 | } else { |
138 | 0 | watcher_.reset(); // Destroy the old watch if any |
139 | 0 | } |
140 | 0 | } |
141 | 0 | } |
142 | 0 | secret_data_.last_updated_ = time_source_.systemTime(); |
143 | 0 | secret_data_.version_info_ = version_info; |
144 | 0 | init_target_.ready(); |
145 | 0 | return absl::OkStatus(); |
146 | 0 | } |
147 | | |
148 | | absl::Status |
149 | | SdsApi::onConfigUpdate(const std::vector<Config::DecodedResourceRef>& added_resources, |
150 | | const Protobuf::RepeatedPtrField<std::string>& removed_resources, |
151 | 0 | const std::string&) { |
152 | 0 | const absl::Status status = validateUpdateSize(added_resources.size(), removed_resources.size()); |
153 | 0 | if (!status.ok()) { |
154 | 0 | return status; |
155 | 0 | } |
156 | | |
157 | 0 | if (removed_resources.size() == 1) { |
158 | | // SDS is a singleton (e.g. single-resource) resource subscription, so it should never be |
159 | | // removed except by the modification of the referenced cluster/listener. Therefore, since the |
160 | | // server indicates a removal, ignore it (via an ACK). |
161 | 0 | ENVOY_LOG_MISC( |
162 | 0 | trace, |
163 | 0 | "Server sent a delta SDS update attempting to remove a resource (name: {}). Ignoring.", |
164 | 0 | removed_resources[0]); |
165 | | |
166 | | // Even if we ignore this resource, the owning resource (LDS/CDS) should still complete |
167 | | // warming. |
168 | 0 | init_target_.ready(); |
169 | 0 | return absl::OkStatus(); |
170 | 0 | } |
171 | 0 | return onConfigUpdate(added_resources, added_resources[0].get().version()); |
172 | 0 | } |
173 | | |
174 | | void SdsApi::onConfigUpdateFailed(Envoy::Config::ConfigUpdateFailureReason reason, |
175 | 0 | const EnvoyException*) { |
176 | 0 | ASSERT(Envoy::Config::ConfigUpdateFailureReason::ConnectionFailure != reason); |
177 | | // We need to allow server startup to continue, even if we have a bad config. |
178 | 0 | init_target_.ready(); |
179 | 0 | } |
180 | | |
181 | | absl::Status SdsApi::validateUpdateSize(uint32_t added_resources_num, |
182 | 0 | uint32_t removed_resources_num) const { |
183 | 0 | if (added_resources_num == 0 && removed_resources_num == 0) { |
184 | 0 | return absl::InvalidArgumentError( |
185 | 0 | fmt::format("Missing SDS resources for {} in onConfigUpdate()", sds_config_name_)); |
186 | 0 | } |
187 | | |
188 | | // This conditional technically allows a response with added=1 removed=1 |
189 | | // which is nonsensical since SDS is a singleton resource subscription. |
190 | | // It is, however, preferred to ignore these nonsensical responses rather |
191 | | // than NACK them, so it is allowed here. |
192 | 0 | if (added_resources_num > 1 || removed_resources_num > 1) { |
193 | 0 | return absl::InvalidArgumentError( |
194 | 0 | fmt::format("Unexpected SDS secrets length for {}, number of added resources " |
195 | 0 | "{}, number of removed resources {}. Expected sum is 1", |
196 | 0 | sds_config_name_, added_resources_num, removed_resources_num)); |
197 | 0 | } |
198 | 0 | return absl::OkStatus(); |
199 | 0 | } |
200 | | |
201 | 0 | void SdsApi::initialize() { |
202 | | // Don't put any code here that can throw exceptions, this has been the cause of multiple |
203 | | // hard-to-diagnose regressions. |
204 | 0 | subscription_->start({sds_config_name_}); |
205 | 0 | } |
206 | | |
207 | 0 | SdsApi::SecretData SdsApi::secretData() { return secret_data_; } |
208 | | |
209 | 0 | SdsApi::FileContentMap SdsApi::loadFiles() { |
210 | 0 | FileContentMap files; |
211 | 0 | for (auto const& filename : getDataSourceFilenames()) { |
212 | 0 | auto file_or_error = api_.fileSystem().fileReadToEnd(filename); |
213 | 0 | THROW_IF_NOT_OK_REF(file_or_error.status()); |
214 | 0 | files[filename] = file_or_error.value(); |
215 | 0 | } |
216 | 0 | return files; |
217 | 0 | } |
218 | | |
219 | 0 | uint64_t SdsApi::getHashForFiles(const FileContentMap& files) { |
220 | 0 | uint64_t hash = 0; |
221 | 0 | for (const auto& it : files) { |
222 | 0 | hash = HashUtil::xxHash64(it.second, hash); |
223 | 0 | } |
224 | 0 | return hash; |
225 | 0 | } |
226 | | |
227 | 0 | std::vector<std::string> TlsCertificateSdsApi::getDataSourceFilenames() { |
228 | 0 | std::vector<std::string> files; |
229 | 0 | if (sds_tls_certificate_secrets_ && sds_tls_certificate_secrets_->has_certificate_chain() && |
230 | 0 | sds_tls_certificate_secrets_->certificate_chain().specifier_case() == |
231 | 0 | envoy::config::core::v3::DataSource::SpecifierCase::kFilename) { |
232 | 0 | files.push_back(sds_tls_certificate_secrets_->certificate_chain().filename()); |
233 | 0 | } |
234 | 0 | if (sds_tls_certificate_secrets_ && sds_tls_certificate_secrets_->has_private_key() && |
235 | 0 | sds_tls_certificate_secrets_->private_key().specifier_case() == |
236 | 0 | envoy::config::core::v3::DataSource::SpecifierCase::kFilename) { |
237 | 0 | files.push_back(sds_tls_certificate_secrets_->private_key().filename()); |
238 | 0 | } |
239 | 0 | return files; |
240 | 0 | } |
241 | | |
242 | 0 | std::vector<std::string> CertificateValidationContextSdsApi::getDataSourceFilenames() { |
243 | 0 | std::vector<std::string> files; |
244 | 0 | if (sds_certificate_validation_context_secrets_) { |
245 | 0 | if (sds_certificate_validation_context_secrets_->has_trusted_ca() && |
246 | 0 | sds_certificate_validation_context_secrets_->trusted_ca().specifier_case() == |
247 | 0 | envoy::config::core::v3::DataSource::SpecifierCase::kFilename) { |
248 | 0 | files.push_back(sds_certificate_validation_context_secrets_->trusted_ca().filename()); |
249 | 0 | } |
250 | 0 | if (sds_certificate_validation_context_secrets_->has_crl() && |
251 | 0 | sds_certificate_validation_context_secrets_->crl().specifier_case() == |
252 | 0 | envoy::config::core::v3::DataSource::SpecifierCase::kFilename) { |
253 | 0 | files.push_back(sds_certificate_validation_context_secrets_->crl().filename()); |
254 | 0 | } |
255 | 0 | } |
256 | 0 | return files; |
257 | 0 | } |
258 | | |
259 | 0 | std::vector<std::string> TlsSessionTicketKeysSdsApi::getDataSourceFilenames() { return {}; } |
260 | | |
261 | 0 | std::vector<std::string> GenericSecretSdsApi::getDataSourceFilenames() { return {}; } |
262 | | |
263 | | } // namespace Secret |
264 | | } // namespace Envoy |