Coverage Report

Created: 2023-11-12 09:30

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