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
285
SdsApiStats SdsApi::generateStats(Stats::Scope& scope) {
16
285
  return {ALL_SDS_API_STATS(POOL_COUNTER(scope))};
17
285
}
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
               bool warm)
24
285
    : Envoy::Config::SubscriptionBase<envoy::extensions::transport_sockets::tls::v3::Secret>(
25
285
          validation_visitor, "name"),
26
285
      init_target_(fmt::format("SdsApi {}", sds_config_name), [this, warm] { initialize(warm); }),
27
285
      dispatcher_(dispatcher), api_(api),
28
285
      scope_(stats.createScope(absl::StrCat("sds.", sds_config_name, "."))),
29
285
      sds_api_stats_(generateStats(*scope_)), sds_config_(std::move(sds_config)),
30
285
      sds_config_name_(sds_config_name), clean_up_(std::move(destructor_cb)),
31
285
      subscription_factory_(subscription_factory), time_source_(time_source),
32
285
      secret_data_{sds_config_name_, "uninitialized", time_source_.systemTime()} {
33
285
  const auto resource_name = getResourceName();
34
  // This has to happen here (rather than in initialize()) as it can throw exceptions.
35
285
  subscription_ = THROW_OR_RETURN_VALUE(
36
285
      subscription_factory_.subscriptionFromConfigSource(
37
285
          sds_config_, Grpc::Common::typeUrl(resource_name), *scope_, *this, resource_decoder_, {}),
38
285
      Config::SubscriptionPtr);
39
285
}
40

            
41
void SdsApi::resolveDataSource(const FileContentMap& files,
42
308
                               envoy::config::core::v3::DataSource& data_source) {
43
308
  if (data_source.specifier_case() ==
44
308
      envoy::config::core::v3::DataSource::SpecifierCase::kFilename) {
45
297
    const std::string& content = files.at(data_source.filename());
46
297
    data_source.set_inline_bytes(content);
47
297
  }
48
308
}
49

            
50
31
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
31
  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
31
    uint64_t prev_hash = 0;
57
31
    FileContentMap files = loadFiles();
58
31
    uint64_t next_hash = getHashForFiles(files);
59
31
    const uint64_t MaxBoundedRetries = 5;
60
31
    for (uint64_t bounded_retries = MaxBoundedRetries;
61
64
         next_hash != prev_hash && bounded_retries > 0; --bounded_retries) {
62
33
      files = loadFiles();
63
33
      prev_hash = next_hash;
64
33
      next_hash = getHashForFiles(files);
65
33
    }
66
31
    if (next_hash != prev_hash) {
67
2
      ENVOY_LOG_MISC(
68
2
          warn, "Unable to atomically refresh secrets due to > {} non-atomic rotations observed",
69
2
          MaxBoundedRetries);
70
2
    }
71
31
    const uint64_t new_hash = next_hash;
72
31
    if (new_hash != files_hash_) {
73
17
      resolveSecret(files);
74
17
      THROW_IF_NOT_OK(update_callback_manager_.runCallbacks());
75
17
      files_hash_ = new_hash;
76
17
      secret_data_.last_updated_ = time_source_.systemTime();
77
      // Signal initialization complete. This should be safe to call multiple times and is
78
      // necessary when we are recovering from initial load failure.
79
17
      init_target_.ready();
80
17
    }
81
31
  }
82
31
  END_TRY
83
31
  CATCH(const EnvoyException& e, {
84
31
    ENVOY_LOG_MISC(warn, fmt::format("Failed to reload certificates: {}", e.what()));
85
31
    sds_api_stats_.key_rotation_failed_.inc();
86
31
  });
87
31
}
88

            
89
absl::Status SdsApi::onConfigUpdate(const std::vector<Config::DecodedResourceRef>& resources,
90
288
                                    const std::string& version_info) {
91
288
  const absl::Status status = validateUpdateSize(resources.size(), 0);
92
288
  if (!status.ok()) {
93
2
    return status;
94
2
  }
95
286
  const auto& secret = dynamic_cast<const envoy::extensions::transport_sockets::tls::v3::Secret&>(
96
286
      resources[0].get().resource());
97

            
98
286
  if (secret.name() != sds_config_name_) {
99
1
    return absl::InvalidArgumentError(
100
1
        fmt::format("Unexpected SDS secret (expecting {}): {}", sds_config_name_, secret.name()));
101
1
  }
102

            
103
285
  const uint64_t new_hash = MessageUtil::hash(secret);
104

            
105
285
  if (new_hash != secret_hash_) {
106
285
    validateConfig(secret);
107
285
    secret_hash_ = new_hash;
108
285
    setSecret(secret);
109
    // Update version_info from xDS before attempting file loads. This would ensure that version
110
    // tracking is available even if files don't exist yet.
111
285
    secret_data_.version_info_ = version_info;
112

            
113
    // Set up per-file watchers before loadFiles() so that if loadFiles() fails, the watches
114
    // are still setup for the next auto-recovery when files appear later.
115
    // For watched_directory case, the callback is already set in setSecret().
116
285
    if (getWatchedDirectory() == nullptr) {
117
      // List DataSources that refer to files.
118
267
      auto datasource_files = getDataSourceFilenames();
119
267
      if (!datasource_files.empty()) {
120
        // Create new watch, also destroys the old watch if any.
121
145
        watcher_ = dispatcher_.createFilesystemWatcher();
122
244
        for (auto const& filename : datasource_files) {
123
          // Watch for directory instead of file. This allows users to do atomic renames
124
          // on directory level (e.g. Kubernetes secret update).
125
244
          const auto result_or_error = api_.fileSystem().splitPathFromFilename(filename);
126
244
          RETURN_IF_NOT_OK_REF(result_or_error.status());
127
244
          RETURN_IF_NOT_OK(watcher_->addWatch(absl::StrCat(result_or_error.value().directory_, "/"),
128
244
                                              Filesystem::Watcher::Events::MovedTo,
129
244
                                              [this](uint32_t) {
130
244
                                                onWatchUpdate();
131
244
                                                return absl::OkStatus();
132
244
                                              }));
133
244
        }
134
253
      } else {
135
122
        watcher_.reset(); // Destroy the old watch if any.
136
122
      }
137
267
    }
138

            
139
285
    const auto files = loadFiles();
140
285
    files_hash_ = getHashForFiles(files);
141
285
    resolveSecret(files);
142
285
    THROW_IF_NOT_OK(update_callback_manager_.runCallbacks());
143
    // Update last_updated only after successful file load and callbacks.
144
277
    secret_data_.last_updated_ = time_source_.systemTime();
145
277
  }
146
277
  init_target_.ready();
147
277
  return absl::OkStatus();
148
285
}
149

            
150
absl::Status
151
SdsApi::onConfigUpdate(const std::vector<Config::DecodedResourceRef>& added_resources,
152
                       const Protobuf::RepeatedPtrField<std::string>& removed_resources,
153
56
                       const std::string&) {
154
56
  const absl::Status status = validateUpdateSize(added_resources.size(), removed_resources.size());
155
56
  if (!status.ok()) {
156
1
    return status;
157
1
  }
158

            
159
55
  if (removed_resources.size() == 1) {
160
    // SDS is a singleton (e.g. single-resource) resource subscription, so it should never be
161
    // removed except by the modification of the referenced cluster/listener. Therefore, since the
162
    // server indicates a removal, ignore it (via an ACK).
163
11
    ENVOY_LOG_MISC(trace, "Server sent a delta SDS update removing a resource (name: {}).",
164
11
                   removed_resources[0]);
165
11
    THROW_IF_NOT_OK(remove_callback_manager_.runCallbacks());
166

            
167
    // Even if we ignore this resource, the owning resource (LDS/CDS) should still complete
168
    // warming.
169
11
    init_target_.ready();
170
11
    return absl::OkStatus();
171
11
  }
172
44
  return onConfigUpdate(added_resources, added_resources[0].get().version());
173
55
}
174

            
175
void SdsApi::onConfigUpdateFailed(Envoy::Config::ConfigUpdateFailureReason reason,
176
25
                                  const EnvoyException*) {
177
25
  ASSERT(Envoy::Config::ConfigUpdateFailureReason::ConnectionFailure != reason);
178
  // We need to allow server startup to continue, even if we have a bad config.
179
25
  init_target_.ready();
180
25
}
181

            
182
absl::Status SdsApi::validateUpdateSize(uint32_t added_resources_num,
183
344
                                        uint32_t removed_resources_num) const {
184
344
  if (added_resources_num == 0 && removed_resources_num == 0) {
185
1
    return absl::InvalidArgumentError(
186
1
        fmt::format("Missing SDS resources for {} in onConfigUpdate()", sds_config_name_));
187
1
  }
188

            
189
  // This conditional technically allows a response with added=1 removed=1
190
  // which is nonsensical since SDS is a singleton resource subscription.
191
  // It is, however, preferred to ignore these nonsensical responses rather
192
  // than NACK them, so it is allowed here.
193
343
  if (added_resources_num > 1 || removed_resources_num > 1) {
194
2
    return absl::InvalidArgumentError(
195
2
        fmt::format("Unexpected SDS secrets length for {}, number of added resources "
196
2
                    "{}, number of removed resources {}. Expected sum is 1",
197
2
                    sds_config_name_, added_resources_num, removed_resources_num));
198
2
  }
199
341
  return absl::OkStatus();
200
343
}
201

            
202
273
void SdsApi::initialize(bool warm) {
203
  // Don't put any code here that can throw exceptions, this has been the cause of multiple
204
  // hard-to-diagnose regressions.
205
273
  subscription_->start({sds_config_name_});
206
273
  if (!warm) {
207
30
    init_target_.ready();
208
30
  }
209
273
}
210

            
211
48
const SdsApi::SecretData& SdsApi::secretData() const { return secret_data_; }
212

            
213
349
SdsApi::FileContentMap SdsApi::loadFiles() {
214
349
  FileContentMap files;
215
506
  for (auto const& filename : getDataSourceFilenames()) {
216
397
    auto file_or_error = api_.fileSystem().fileReadToEnd(filename);
217
397
    THROW_IF_NOT_OK_REF(file_or_error.status());
218
393
    files[filename] = file_or_error.value();
219
393
  }
220
345
  return files;
221
349
}
222

            
223
336
uint64_t SdsApi::getHashForFiles(const FileContentMap& files) {
224
336
  uint64_t hash = 0;
225
491
  for (const auto& it : files) {
226
382
    hash = HashUtil::xxHash64(it.second, hash);
227
382
  }
228
336
  return hash;
229
336
}
230

            
231
TlsCertificateSdsApiSharedPtr
232
TlsCertificateSdsApi::create(Server::Configuration::ServerFactoryContext& server_context,
233
                             const envoy::config::core::v3::ConfigSource& sds_config,
234
                             const std::string& sds_config_name,
235
109
                             std::function<void()> destructor_cb, bool warm) {
236
  // We need to do this early as we invoke the subscription factory during initialization, which
237
  // is too late to throw.
238
109
  THROW_IF_NOT_OK(
239
109
      Config::Utility::checkLocalInfo("TlsCertificateSdsApi", server_context.localInfo()));
240
109
  return std::make_shared<TlsCertificateSdsApi>(
241
109
      sds_config, sds_config_name, server_context.clusterManager().subscriptionFactory(),
242
109
      server_context.mainThreadDispatcher().timeSource(), server_context.messageValidationVisitor(),
243
109
      server_context.serverScope().store(), destructor_cb, server_context.mainThreadDispatcher(),
244
109
      server_context.api(), warm);
245
109
}
246

            
247
296
std::vector<std::string> TlsCertificateSdsApi::getDataSourceFilenames() {
248
296
  std::vector<std::string> files;
249
296
  if (sds_tls_certificate_secrets_ && sds_tls_certificate_secrets_->has_certificate_chain() &&
250
296
      sds_tls_certificate_secrets_->certificate_chain().specifier_case() ==
251
280
          envoy::config::core::v3::DataSource::SpecifierCase::kFilename) {
252
278
    files.push_back(sds_tls_certificate_secrets_->certificate_chain().filename());
253
278
  }
254
296
  if (sds_tls_certificate_secrets_ && sds_tls_certificate_secrets_->has_private_key() &&
255
296
      sds_tls_certificate_secrets_->private_key().specifier_case() ==
256
270
          envoy::config::core::v3::DataSource::SpecifierCase::kFilename) {
257
268
    files.push_back(sds_tls_certificate_secrets_->private_key().filename());
258
268
  }
259
296
  return files;
260
296
}
261

            
262
void TlsCertificateSdsApi::setSecret(
263
127
    const envoy::extensions::transport_sockets::tls::v3::Secret& secret) {
264
127
  sds_tls_certificate_secrets_ =
265
127
      std::make_unique<envoy::extensions::transport_sockets::tls::v3::TlsCertificate>(
266
127
          secret.tls_certificate());
267
127
  resolved_tls_certificate_secrets_ = nullptr;
268
127
  if (secret.tls_certificate().has_watched_directory()) {
269
18
    watched_directory_ = THROW_OR_RETURN_VALUE(
270
18
        Config::WatchedDirectory::create(secret.tls_certificate().watched_directory(), dispatcher_),
271
18
        std::unique_ptr<Config::WatchedDirectory>);
272
    // Set the callback immediately so that if subsequent operations fail, the watch is
273
    // still active and can trigger recovery when files appear later.
274
18
    watched_directory_->setCallback([this]() {
275
16
      onWatchUpdate();
276
16
      return absl::OkStatus();
277
16
    });
278
109
  } else {
279
109
    watched_directory_.reset();
280
109
  }
281
127
}
282

            
283
137
void TlsCertificateSdsApi::resolveSecret(const FileContentMap& files) {
284
137
  resolved_tls_certificate_secrets_ =
285
137
      std::make_unique<envoy::extensions::transport_sockets::tls::v3::TlsCertificate>(
286
137
          *sds_tls_certificate_secrets_);
287
  // We replace path based secrets with inlined secrets on update.
288
137
  resolveDataSource(files, *resolved_tls_certificate_secrets_->mutable_certificate_chain());
289
137
  if (sds_tls_certificate_secrets_->has_private_key()) {
290
124
    resolveDataSource(files, *resolved_tls_certificate_secrets_->mutable_private_key());
291
124
  }
292
137
}
293

            
294
CertificateValidationContextSdsApiSharedPtr CertificateValidationContextSdsApi::create(
295
    Server::Configuration::ServerFactoryContext& server_context,
296
    const envoy::config::core::v3::ConfigSource& sds_config, const std::string& sds_config_name,
297
37
    std::function<void()> destructor_cb, bool warm) {
298
  // We need to do this early as we invoke the subscription factory during initialization, which
299
  // is too late to throw.
300
37
  THROW_IF_NOT_OK(Config::Utility::checkLocalInfo("CertificateValidationContextSdsApi",
301
37
                                                  server_context.localInfo()));
302
37
  return std::make_shared<CertificateValidationContextSdsApi>(
303
37
      sds_config, sds_config_name, server_context.clusterManager().subscriptionFactory(),
304
37
      server_context.mainThreadDispatcher().timeSource(), server_context.messageValidationVisitor(),
305
37
      server_context.serverScope().store(), destructor_cb, server_context.mainThreadDispatcher(),
306
37
      server_context.api(), warm);
307
37
}
308

            
309
void CertificateValidationContextSdsApi::validateConfig(
310
43
    const envoy::extensions::transport_sockets::tls::v3::Secret& secret) {
311
43
  THROW_IF_NOT_OK(validation_callback_manager_.runCallbacks(secret.validation_context()));
312
43
}
313

            
314
void CertificateValidationContextSdsApi::setSecret(
315
43
    const envoy::extensions::transport_sockets::tls::v3::Secret& secret) {
316
43
  sds_certificate_validation_context_secrets_ =
317
43
      std::make_unique<envoy::extensions::transport_sockets::tls::v3::CertificateValidationContext>(
318
43
          secret.validation_context());
319
43
  resolved_certificate_validation_context_secrets_ = nullptr;
320
43
  if (secret.validation_context().has_watched_directory()) {
321
    watched_directory_ =
322
        THROW_OR_RETURN_VALUE(Config::WatchedDirectory::create(
323
                                  secret.validation_context().watched_directory(), dispatcher_),
324
                              std::unique_ptr<Config::WatchedDirectory>);
325
    // Set the callback immediately so that if subsequent operations fail, the watch is
326
    // still active and can trigger recovery when files appear later.
327
    watched_directory_->setCallback([this]() {
328
      onWatchUpdate();
329
      return absl::OkStatus();
330
    });
331
43
  } else {
332
43
    watched_directory_.reset();
333
43
  }
334
43
}
335

            
336
45
void CertificateValidationContextSdsApi::resolveSecret(const FileContentMap& files) {
337
  // Copy existing CertificateValidationContext.
338
45
  resolved_certificate_validation_context_secrets_ =
339
45
      std::make_unique<envoy::extensions::transport_sockets::tls::v3::CertificateValidationContext>(
340
45
          *sds_certificate_validation_context_secrets_);
341
  // We replace path based secrets with inlined secrets on update.
342
45
  if (sds_certificate_validation_context_secrets_->has_trusted_ca()) {
343
43
    resolveDataSource(files,
344
43
                      *resolved_certificate_validation_context_secrets_->mutable_trusted_ca());
345
43
  }
346
45
  if (sds_certificate_validation_context_secrets_->has_crl()) {
347
4
    resolveDataSource(files, *resolved_certificate_validation_context_secrets_->mutable_crl());
348
4
  }
349
45
}
350

            
351
90
std::vector<std::string> CertificateValidationContextSdsApi::getDataSourceFilenames() {
352
90
  std::vector<std::string> files;
353
90
  if (sds_certificate_validation_context_secrets_) {
354
90
    if (sds_certificate_validation_context_secrets_->has_trusted_ca() &&
355
90
        sds_certificate_validation_context_secrets_->trusted_ca().specifier_case() ==
356
86
            envoy::config::core::v3::DataSource::SpecifierCase::kFilename) {
357
84
      files.push_back(sds_certificate_validation_context_secrets_->trusted_ca().filename());
358
84
    }
359
90
    if (sds_certificate_validation_context_secrets_->has_crl() &&
360
90
        sds_certificate_validation_context_secrets_->crl().specifier_case() ==
361
8
            envoy::config::core::v3::DataSource::SpecifierCase::kFilename) {
362
8
      files.push_back(sds_certificate_validation_context_secrets_->crl().filename());
363
8
    }
364
90
  }
365
90
  return files;
366
90
}
367

            
368
TlsSessionTicketKeysSdsApiSharedPtr
369
TlsSessionTicketKeysSdsApi::create(Server::Configuration::ServerFactoryContext& server_context,
370
                                   const envoy::config::core::v3::ConfigSource& sds_config,
371
                                   const std::string& sds_config_name,
372
3
                                   std::function<void()> destructor_cb, bool warm) {
373
  // We need to do this early as we invoke the subscription factory during initialization, which
374
  // is too late to throw.
375
3
  THROW_IF_NOT_OK(
376
3
      Config::Utility::checkLocalInfo("TlsSessionTicketKeysSdsApi", server_context.localInfo()));
377
3
  return std::make_shared<TlsSessionTicketKeysSdsApi>(
378
3
      sds_config, sds_config_name, server_context.clusterManager().subscriptionFactory(),
379
3
      server_context.mainThreadDispatcher().timeSource(), server_context.messageValidationVisitor(),
380
3
      server_context.serverScope().store(), destructor_cb, server_context.mainThreadDispatcher(),
381
3
      server_context.api(), warm);
382
3
}
383

            
384
void TlsSessionTicketKeysSdsApi::validateConfig(
385
1
    const envoy::extensions::transport_sockets::tls::v3::Secret& secret) {
386
1
  THROW_IF_NOT_OK(validation_callback_manager_.runCallbacks(secret.session_ticket_keys()));
387
1
}
388

            
389
2
std::vector<std::string> TlsSessionTicketKeysSdsApi::getDataSourceFilenames() { return {}; }
390

            
391
GenericSecretSdsApiSharedPtr
392
GenericSecretSdsApi::create(Server::Configuration::ServerFactoryContext& server_context,
393
                            const envoy::config::core::v3::ConfigSource& sds_config,
394
                            const std::string& sds_config_name, std::function<void()> destructor_cb,
395
98
                            bool warm) {
396
  // We need to do this early as we invoke the subscription factory during initialization, which
397
  // is too late to throw.
398
98
  THROW_IF_NOT_OK(
399
98
      Config::Utility::checkLocalInfo("GenericSecretSdsApi", server_context.localInfo()));
400
98
  return std::make_shared<GenericSecretSdsApi>(
401
98
      sds_config, sds_config_name, server_context.clusterManager().subscriptionFactory(),
402
98
      server_context.mainThreadDispatcher().timeSource(), server_context.messageValidationVisitor(),
403
98
      server_context.serverScope().store(), destructor_cb, server_context.mainThreadDispatcher(),
404
98
      server_context.api(), warm);
405
98
}
406

            
407
void GenericSecretSdsApi::validateConfig(
408
114
    const envoy::extensions::transport_sockets::tls::v3::Secret& secret) {
409
114
  THROW_IF_NOT_OK(validation_callback_manager_.runCallbacks(secret.generic_secret()));
410
114
}
411

            
412
228
std::vector<std::string> GenericSecretSdsApi::getDataSourceFilenames() {
413
228
  std::vector<std::string> files;
414

            
415
228
  ASSERT(generic_secret_ != nullptr);
416

            
417
228
  if (generic_secret_->secret().has_filename()) {
418
6
    files.push_back(generic_secret_->secret().filename());
419
224
  } else {
420
226
    for (const auto& entry : generic_secret_->secrets()) {
421
16
      if (entry.second.has_filename()) {
422
8
        files.push_back(entry.second.filename());
423
8
      }
424
16
    }
425
222
  }
426
228
  return files;
427
228
}
428

            
429
} // namespace Secret
430
} // namespace Envoy