Coverage Report

Created: 2024-09-19 09:45

/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