1
#include <fmt/base.h>
2
#include <fmt/format.h>
3
#include <gtest/gtest-param-test.h>
4
#include <gtest/gtest.h>
5

            
6
#include <string>
7
#include <utility>
8
#include <vector>
9

            
10
#include "envoy/network/address.h"
11

            
12
#include "source/common/common/logger.h"
13
#include "source/common/network/utility.h"
14

            
15
#include "test/integration/http_integration.h"
16
#include "test/test_common/environment.h"
17
#include "test/test_common/utility.h"
18

            
19
#include "absl/time/clock.h"
20
#include "absl/time/time.h"
21
#include "absl/types/optional.h"
22
#include "cilium/api/accesslog.pb.h"
23
#include "cilium/secret_watcher.h"
24
#include "tests/bpf_metadata.h" // host_map_config
25
#include "tests/cilium_http_integration.h"
26

            
27
namespace Envoy {
28

            
29
// params: destination port number
30
const std::string BASIC_POLICY_fmt = R"EOF(version_info: "0"
31
resources:
32
- "@type": type.googleapis.com/cilium.NetworkPolicy
33
  endpoint_ips:
34
  - '{{ ntop_ip_loopback_address }}'
35
  endpoint_id: 3
36
  ingress_per_port_policies:
37
  - port: {0}
38
    rules:
39
    - remote_policies: [ 1 ]
40
      http_rules:
41
        http_rules:
42
        - headers:
43
          - name: ':path'
44
            exact_match: '/allowed'
45
        - headers:
46
          - name: ':path'
47
            safe_regex_match:
48
              google_re2: {{}}
49
              regex: '.*public$'
50
        - headers:
51
          - name: ':authority'
52
            exact_match: 'allowedHOST'
53
        - headers:
54
          - name: ':authority'
55
            safe_regex_match:
56
              google_re2: {{}}
57
              regex: '.*REGEX.*'
58
        - headers:
59
          - name: ':method'
60
            exact_match: 'PUT'
61
          - name: ':path'
62
            exact_match: '/public/opinions'
63
    - remote_policies: [ 2 ]
64
      http_rules:
65
        http_rules:
66
        - headers:
67
          - name: ':path'
68
            exact_match: '/only-2-allowed'
69
  egress_per_port_policies:
70
  - port: {0}
71
    rules:
72
    - remote_policies: [ 1 ]
73
      http_rules:
74
        http_rules:
75
        - headers:
76
          - name: ':path'
77
            exact_match: '/allowed'
78
        - headers:
79
          - name: ':path'
80
            safe_regex_match:
81
              google_re2: {{}}
82
              regex: '.*public$'
83
        - headers:
84
          - name: ':authority'
85
            exact_match: 'allowedHOST'
86
        - headers:
87
          - name: ':authority'
88
            safe_regex_match:
89
              google_re2: {{}}
90
              regex: '.*REGEX.*'
91
        - headers:
92
          - name: ':method'
93
            exact_match: 'PUT'
94
          - name: ':path'
95
            exact_match: '/public/opinions'
96
    - remote_policies: [ 2 ]
97
      http_rules:
98
        http_rules:
99
        - headers:
100
          - name: ':path'
101
            exact_match: '/only-2-allowed'
102
)EOF";
103

            
104
const std::string SECRET_TOKEN_CONFIG = R"EOF(version_info: "0"
105
resources:
106
- "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.Secret
107
  name: bearer-token
108
  generic_secret:
109
    secret:
110
      inline_string: "d4ef0f5011f163ac"
111
)EOF";
112

            
113
const std::string HEADER_ACTION_POLICY_fmt = R"EOF(version_info: "0"
114
resources:
115
- "@type": type.googleapis.com/cilium.NetworkPolicy
116
  endpoint_ips:
117
  - '{{ ntop_ip_loopback_address }}'
118
  endpoint_id: 3
119
  ingress_per_port_policies:
120
  - port: {0}
121
    rules:
122
    - remote_policies: [ 1 ]
123
      http_rules:
124
        http_rules:
125
        - headers:
126
          - name: ':path'
127
            exact_match: '/allowed'
128
          header_matches:
129
          - name: 'header42'
130
            match_action: FAIL_ON_MATCH
131
            mismatch_action: CONTINUE_ON_MISMATCH
132
          - name: 'bearer-token'
133
            value_sds_secret: 'bearer-token'
134
            mismatch_action: REPLACE_ON_MISMATCH
135
        - headers:
136
          - name: ':path'
137
            safe_regex_match:
138
              google_re2: {{}}
139
              regex: '.*public$'
140
          header_matches:
141
          - name: 'user-agent'
142
            value: 'CuRL'
143
            mismatch_action: DELETE_ON_MISMATCH
144
        - headers:
145
          - name: ':authority'
146
            exact_match: 'allowedHOST'
147
          header_matches:
148
          - name: 'header2'
149
            value: 'value2'
150
            mismatch_action: ADD_ON_MISMATCH
151
          - name: 'header42'
152
            match_action: DELETE_ON_MATCH
153
            mismatch_action: CONTINUE_ON_MISMATCH
154
        - headers:
155
          - name: ':authority'
156
            safe_regex_match:
157
              google_re2: {{}}
158
              regex: '.*REGEX.*'
159
          header_matches:
160
          - name: 'header42'
161
            value: '42'
162
            mismatch_action: DELETE_ON_MISMATCH
163
        - headers:
164
          - name: ':method'
165
            exact_match: 'PUT'
166
          - name: ':path'
167
            exact_match: '/public/opinions'
168
    - remote_policies: [ 2 ]
169
      http_rules:
170
        http_rules:
171
        - headers:
172
          - name: ':path'
173
            exact_match: '/only-2-allowed'
174
  egress_per_port_policies:
175
  - port: {0}
176
    rules:
177
    - remote_policies: [ 1 ]
178
      http_rules:
179
        http_rules:
180
        - headers:
181
          - name: ':path'
182
            exact_match: '/allowed'
183
          header_matches:
184
          - name: 'header42'
185
            match_action: FAIL_ON_MATCH
186
            mismatch_action: CONTINUE_ON_MISMATCH
187
          - name: 'bearer-token'
188
            value_sds_secret: 'bearer-token'
189
            mismatch_action: REPLACE_ON_MISMATCH
190
        - headers:
191
          - name: ':path'
192
            safe_regex_match:
193
              google_re2: {{}}
194
              regex: '.*public$'
195
          header_matches:
196
          - name: 'user-agent'
197
            value: 'CuRL'
198
            mismatch_action: DELETE_ON_MISMATCH
199
        - headers:
200
          - name: ':authority'
201
            exact_match: 'allowedHOST'
202
          header_matches:
203
          - name: 'header2'
204
            value: 'value2'
205
            mismatch_action: ADD_ON_MISMATCH
206
          - name: 'header42'
207
            match_action: DELETE_ON_MATCH
208
            mismatch_action: CONTINUE_ON_MISMATCH
209
        - headers:
210
          - name: ':authority'
211
            safe_regex_match:
212
              google_re2: {{}}
213
              regex: '.*REGEX.*'
214
          header_matches:
215
          - name: 'header42'
216
            value: '42'
217
            mismatch_action: DELETE_ON_MISMATCH
218
        - headers:
219
          - name: ':method'
220
            exact_match: 'PUT'
221
          - name: ':path'
222
            exact_match: '/public/opinions'
223
    - remote_policies: [ 2 ]
224
      http_rules:
225
        http_rules:
226
        - headers:
227
          - name: ':path'
228
            exact_match: '/only-2-allowed'
229
)EOF";
230

            
231
// params: is_ingress ("true", "false")
232
const std::string cilium_upstream_config_fmt = R"EOF(
233
admin:
234
  address:
235
    socket_address:
236
      address: 127.0.0.1
237
      port_value: 0
238
static_resources:
239
  clusters:
240
  - name: cluster1
241
    type: ORIGINAL_DST
242
    lb_policy: CLUSTER_PROVIDED
243
    connect_timeout:
244
      seconds: 1
245
    typed_extension_protocol_options:
246
      envoy.extensions.upstreams.http.v3.HttpProtocolOptions:
247
        "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions
248
        common_http_protocol_options:
249
          max_requests_per_connection: 3
250
        use_downstream_protocol_config: {{}}
251
        http_filters:
252
        - name: test_l7policy
253
          typed_config:
254
            "@type": type.googleapis.com/cilium.L7Policy
255
            access_log_path: "{{ test_udsdir }}/access_log.sock"
256
        - name: envoy.filters.http.upstream_codec
257
          typed_config:
258
            "@type": type.googleapis.com/envoy.extensions.filters.http.upstream_codec.v3.UpstreamCodec
259
  - name: xds-grpc-cilium
260
    connect_timeout:
261
      seconds: 5
262
    type: STATIC
263
    lb_policy: ROUND_ROBIN
264
    http2_protocol_options:
265
    load_assignment:
266
      cluster_name: xds-grpc-cilium
267
      endpoints:
268
      - lb_endpoints:
269
        - endpoint:
270
            address:
271
              pipe:
272
                path: /var/run/cilium/xds.sock
273
  listeners:
274
    name: http
275
    address:
276
      socket_address:
277
        address: 127.0.0.1
278
        port_value: 0
279
    listener_filters:
280
      name: test_bpf_metadata
281
      typed_config:
282
        "@type": type.googleapis.com/cilium.TestBpfMetadata
283
        is_ingress: {0}
284
        is_l7lb: true
285
    filter_chains:
286
      filters:
287
      - name: cilium.network
288
        typed_config:
289
          "@type": type.googleapis.com/cilium.NetworkFilter
290
      - name: envoy.http_connection_manager
291
        typed_config:
292
          "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
293
          stat_prefix: config_test
294
          codec_type: auto
295
          use_remote_address: true
296
          skip_xff_append: true
297
          http_filters:
298
          - name: test_l7policy
299
            typed_config:
300
              "@type": type.googleapis.com/cilium.L7Policy
301
              access_log_path: "{{ test_udsdir }}/access_log.sock"
302
          - name: envoy.filters.http.router
303
            typed_config:
304
              "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
305
          route_config:
306
            name: policy_enabled
307
            virtual_hosts:
308
              name: integration
309
              domains: "*"
310
              routes:
311
              - route:
312
                  cluster: cluster1
313
                  max_grpc_timeout:
314
                    seconds: 0
315
                    nanos: 0
316
                match:
317
                  prefix: "/"
318
)EOF";
319

            
320
class CiliumIntegrationTest : public CiliumHttpIntegrationTest {
321
public:
322
  CiliumIntegrationTest()
323
19
      : CiliumHttpIntegrationTest(fmt::format(
324
19
            fmt::runtime(TestEnvironment::substitute(cilium_upstream_config_fmt, GetParam())),
325
19
            "false")) {
326
19
    host_map_config = R"EOF(version_info: "0"
327
19
resources:
328
19
- "@type": type.googleapis.com/cilium.NetworkPolicyHosts
329
19
  policy: 173
330
19
  host_addresses: [ "192.168.0.1", "f00d::1" ]
331
19
- "@type": type.googleapis.com/cilium.NetworkPolicyHosts
332
19
  policy: 1
333
19
  host_addresses: [ "127.0.0.0/8", "::/104" ]
334
19
)EOF";
335
19
  }
336
11
  CiliumIntegrationTest(const std::string& config) : CiliumHttpIntegrationTest(config) {}
337

            
338
25
  std::string testPolicyFmt() override {
339
25
    return TestEnvironment::substitute(HEADER_ACTION_POLICY_fmt, GetParam());
340
25
  }
341

            
342
27
  std::vector<std::pair<std::string, std::string>> testSecrets() override {
343
27
    return std::vector<std::pair<std::string, std::string>>{
344
27
        {"bearer-token", SECRET_TOKEN_CONFIG},
345
27
    };
346
27
  }
347

            
348
33
  void initialize() override {
349
33
    accessLogServer_.clear();
350
33
    if (!initialized_) {
351
30
      HttpIntegrationTest::initialize();
352
30
      initialized_ = true;
353
30
    }
354
33
  }
355

            
356
15
  void denied(Http::TestRequestHeaderMapImpl&& headers) {
357
15
    initialize();
358
15
    codec_client_ = makeHttpConnection(lookupPort("http"));
359
15
    auto response = codec_client_->makeHeaderOnlyRequest(headers);
360
15
    ASSERT_TRUE(response->waitForEndStream());
361

            
362
    // Validate that request access log message with x-request-id is logged
363
15
    absl::optional<std::string> maybe_x_request_id;
364
15
    EXPECT_TRUE(expectAccessLogDeniedTo([&maybe_x_request_id](const ::cilium::LogEntry& entry) {
365
15
      maybe_x_request_id = getHeader(entry.http().headers(), "x-request-id");
366
15
      return entry.http().status() == 0;
367
15
    }));
368
15
    ASSERT_TRUE(maybe_x_request_id.has_value());
369

            
370
    // Validate that response x-request-id is the same as in request
371
15
    absl::optional<std::string> maybe_x_request_id_resp;
372
15
    EXPECT_TRUE(
373
15
        expectAccessLogResponseTo([&maybe_x_request_id_resp](const ::cilium::LogEntry& entry) {
374
15
          maybe_x_request_id_resp = getHeader(entry.http().headers(), "x-request-id");
375
15
          return entry.http().status() == 403;
376
15
        }));
377
15
    ASSERT_TRUE(maybe_x_request_id_resp.has_value());
378
15
    EXPECT_EQ(maybe_x_request_id.value(), maybe_x_request_id_resp.value());
379

            
380
15
    EXPECT_TRUE(response->complete());
381
15
    EXPECT_EQ("403", response->headers().getStatusValue());
382
15
    cleanupUpstreamAndDownstream();
383
15
  }
384

            
385
18
  void accepted(Http::TestRequestHeaderMapImpl&& headers) {
386
18
    initialize();
387
18
    codec_client_ = makeHttpConnection(lookupPort("http"));
388
18
    auto response = sendRequestAndWaitForResponse(headers, 0, default_response_headers_, 0);
389

            
390
    // Validate that request access log message with x-request-id is logged
391
18
    absl::optional<std::string> maybe_x_request_id;
392
18
    EXPECT_TRUE(expectAccessLogRequestTo([&maybe_x_request_id](const ::cilium::LogEntry& entry) {
393
18
      maybe_x_request_id = getHeader(entry.http().headers(), "x-request-id");
394
18
      return entry.http().status() == 0;
395
18
    }));
396
18
    ASSERT_TRUE(maybe_x_request_id.has_value());
397

            
398
    // Validate that response x-request-id is the same as in request
399
18
    absl::optional<std::string> maybe_x_request_id_resp;
400
18
    EXPECT_TRUE(
401
18
        expectAccessLogResponseTo([&maybe_x_request_id_resp](const ::cilium::LogEntry& entry) {
402
18
          maybe_x_request_id_resp = getHeader(entry.http().headers(), "x-request-id");
403
18
          return entry.http().status() == 200;
404
18
        }));
405
18
    ASSERT_TRUE(maybe_x_request_id_resp.has_value());
406
18
    EXPECT_EQ(maybe_x_request_id.value(), maybe_x_request_id_resp.value());
407

            
408
18
    EXPECT_TRUE(response->complete());
409
18
    EXPECT_EQ("200", response->headers().getStatusValue());
410
18
    EXPECT_TRUE(upstream_request_->complete());
411
18
    EXPECT_EQ(0, upstream_request_->bodyLength());
412
18
    cleanupUpstreamAndDownstream();
413
18
  }
414

            
415
  bool initialized_ = false;
416
};
417

            
418
INSTANTIATE_TEST_SUITE_P(IpVersions, CiliumIntegrationTest,
419
                         testing::ValuesIn(TestEnvironment::getIpVersionsForTest()));
420

            
421
1
TEST_P(CiliumIntegrationTest, DeniedPathPrefix) {
422
1
  denied({{":method", "GET"}, {":path", "/prefix"}, {":authority", "host"}});
423

            
424
  // Validate that missing headers are access logged correctly
425
1
  EXPECT_TRUE(expectAccessLogDeniedTo([](const ::cilium::LogEntry& entry) {
426
1
    const auto& http = entry.http();
427
1
    return http.missing_headers_size() == 0 && http.rejected_headers_size() == 0;
428
1
  }));
429
1
}
430

            
431
1
TEST_P(CiliumIntegrationTest, AllowedPathPrefix) {
432
1
  accepted({{":method", "GET"},
433
1
            {":path", "/allowed"},
434
1
            {":authority", "host"},
435
1
            {"bearer-token", "d4ef0f5011f163ac"}});
436

            
437
  // Validate that missing headers are access logged correctly
438
1
  EXPECT_TRUE(expectAccessLogRequestTo([](const ::cilium::LogEntry& entry) {
439
1
    const auto& http = entry.http();
440
1
    const auto& missing = http.missing_headers();
441
1
    return http.missing_headers_size() == 1 && hasHeader(missing, "header42") &&
442
1
           http.rejected_headers_size() == 0 && !hasHeader(http.headers(), "header42");
443
1
  }));
444
1
}
445

            
446
1
TEST_P(CiliumIntegrationTest, AllowedPathPrefixWrongHeader) {
447
1
  accepted({{":method", "GET"},
448
1
            {":path", "/allowed"},
449
1
            {":authority", "host"},
450
1
            {"bearer-token", "wrong-value"},
451
1
            {"x-envoy-original-dst-host", "1.1.1.1:9999"}});
452

            
453
  // Validate that missing headers are access logged correctly
454
1
  EXPECT_TRUE(expectAccessLogRequestTo([](const ::cilium::LogEntry& entry) {
455
1
    const auto& http = entry.http();
456
1
    const auto& rejected = http.rejected_headers();
457
1
    const auto& missing = http.missing_headers();
458
1
    return http.rejected_headers_size() == 1 && hasHeader(rejected, "bearer-token", "[redacted]") &&
459
1
           http.missing_headers_size() == 2 && hasHeader(missing, "header42") &&
460
1
           hasHeader(missing, "bearer-token", "[redacted]") &&
461
           // Check that logged headers have the replaced value
462
1
           hasHeader(http.headers(), "bearer-token", "d4ef0f5011f163ac") &&
463
1
           !hasHeader(http.headers(), "header42");
464
1
  }));
465
1
}
466

            
467
1
TEST_P(CiliumIntegrationTest, MultipleRequests) {
468
  // 1st request
469
1
  accepted({{":method", "GET"},
470
1
            {":path", "/allowed"},
471
1
            {":authority", "host"},
472
1
            {"bearer-token", "d4ef0f5011f163ac"}});
473

            
474
  // Validate that missing headers are access logged correctly
475
1
  EXPECT_TRUE(expectAccessLogRequestTo([](const ::cilium::LogEntry& entry) {
476
1
    const auto& http = entry.http();
477
1
    const auto& missing = http.missing_headers();
478
1
    return http.missing_headers_size() == 1 && hasHeader(missing, "header42") &&
479
1
           http.rejected_headers_size() == 0 && !hasHeader(http.headers(), "header42");
480
1
  }));
481

            
482
  // 2nd request
483
1
  accepted({{":method", "GET"},
484
1
            {":path", "/allowed"},
485
1
            {":authority", "host"},
486
1
            {"bearer-token", "wrong-value"},
487
1
            {"x-envoy-original-dst-host", "1.1.1.1:9999"}});
488

            
489
  // Validate that missing headers are access logged correctly
490
1
  EXPECT_TRUE(expectAccessLogRequestTo([](const ::cilium::LogEntry& entry) {
491
1
    const auto& http = entry.http();
492
1
    const auto& rejected = http.rejected_headers();
493
1
    const auto& missing = http.missing_headers();
494
1
    return http.rejected_headers_size() == 1 && hasHeader(rejected, "bearer-token", "[redacted]") &&
495
1
           http.missing_headers_size() == 2 && hasHeader(missing, "header42") &&
496
1
           hasHeader(missing, "bearer-token", "[redacted]") &&
497
           // Check that logged headers have the replaced value
498
1
           hasHeader(http.headers(), "bearer-token", "d4ef0f5011f163ac") &&
499
1
           !hasHeader(http.headers(), "header42");
500
1
  }));
501
1
}
502

            
503
1
TEST_P(CiliumIntegrationTest, AllowedPathRegex) {
504
1
  accepted({{":method", "GET"}, {":path", "/maybe/public"}, {":authority", "host"}});
505

            
506
  // Validate that missing headers are access logged correctly
507
1
  EXPECT_TRUE(expectAccessLogRequestTo([](const ::cilium::LogEntry& entry) {
508
1
    const auto& http = entry.http();
509
1
    return http.rejected_headers_size() == 0 && http.missing_headers_size() == 0;
510
1
  }));
511
1
}
512

            
513
1
TEST_P(CiliumIntegrationTest, AllowedPathRegexDeleteHeader) {
514
1
  accepted({{":method", "GET"},
515
1
            {":path", "/maybe/public"},
516
1
            {":authority", "host"},
517
1
            {"User-Agent", "test"}});
518

            
519
  // Validate that missing headers are access logged correctly
520
1
  EXPECT_TRUE(expectAccessLogRequestTo([](const ::cilium::LogEntry& entry) {
521
1
    const auto& http = entry.http();
522
1
    const auto& rejected = http.rejected_headers();
523
1
    return http.missing_headers_size() == 0 && http.rejected_headers_size() == 1 &&
524
1
           hasHeader(rejected, "user-agent", "test") && !hasHeader(http.headers(), "User-Agent");
525
1
  }));
526
1
}
527

            
528
1
TEST_P(CiliumIntegrationTest, AllowedHostRegexDeleteHeader) {
529
1
  accepted({{":method", "GET"},
530
1
            {":path", "/maybe/private"},
531
1
            {":authority", "hostREGEXname"},
532
1
            {"header42", "test"}});
533

            
534
  // Validate that missing headers are access logged correctly
535
1
  EXPECT_TRUE(expectAccessLogRequestTo([](const ::cilium::LogEntry& entry) {
536
1
    const auto& http = entry.http();
537
1
    const auto& rejected = http.rejected_headers();
538
1
    return http.missing_headers_size() == 0 && http.rejected_headers_size() == 1 &&
539
1
           hasHeader(rejected, "header42", "test") &&
540
1
           !hasHeader(http.headers(), "header42", "test");
541
1
  }));
542
1
}
543

            
544
1
TEST_P(CiliumIntegrationTest, DeniedPath) {
545
1
  denied({{":method", "GET"}, {":path", "/maybe/private"}, {":authority", "host"}});
546

            
547
  // Validate that missing headers are access logged correctly
548
1
  EXPECT_TRUE(expectAccessLogDeniedTo([](const ::cilium::LogEntry& entry) {
549
1
    const auto& http = entry.http();
550
1
    return http.missing_headers_size() == 0 && http.rejected_headers_size() == 0;
551
1
  }));
552
1
}
553

            
554
1
TEST_P(CiliumIntegrationTest, AllowedHostString) {
555
1
  accepted({{":method", "GET"}, {":path", "/maybe/private"}, {":authority", "allowedHOST"}});
556

            
557
  // Validate that missing headers are access logged correctly
558
1
  EXPECT_TRUE(expectAccessLogRequestTo([](const ::cilium::LogEntry& entry) {
559
1
    const auto& http = entry.http();
560
1
    const auto& missing = http.missing_headers();
561
1
    return http.missing_headers_size() == 2 && hasHeader(missing, "header2", "value2") &&
562
1
           hasHeader(missing, "header42") && http.rejected_headers_size() == 0 &&
563
1
           !hasHeader(http.headers(), "header42") && hasHeader(http.headers(), "header2", "value2");
564
1
  }));
565
1
}
566

            
567
1
TEST_P(CiliumIntegrationTest, AllowedReplaced) {
568
1
  accepted({{":method", "GET"}, {":path", "/allowed"}, {":authority", "allowedHOST"}});
569

            
570
  // Validate that missing headers are access logged correctly
571
1
  EXPECT_TRUE(expectAccessLogRequestTo([](const ::cilium::LogEntry& entry) {
572
1
    const auto& http = entry.http();
573
1
    const auto& missing = http.missing_headers();
574
1
    return http.missing_headers_size() == 3 && hasHeader(missing, "bearer-token", "[redacted]") &&
575
1
           hasHeader(missing, "header2", "value2") && hasHeader(missing, "header42") &&
576
1
           http.rejected_headers_size() == 0 && !hasHeader(http.headers(), "header42") &&
577
1
           hasHeader(http.headers(), "header2", "value2") &&
578
1
           hasHeader(http.headers(), "bearer-token", "d4ef0f5011f163ac");
579
1
  }));
580
1
}
581

            
582
1
TEST_P(CiliumIntegrationTest, Denied42) {
583
1
  denied({{":method", "GET"},
584
1
          {":path", "/allowed"},
585
1
          {":authority", "host"},
586
1
          {"header42", "anything"}});
587

            
588
  // Validate that missing headers are access logged correctly
589
1
  EXPECT_TRUE(expectAccessLogDeniedTo([](const ::cilium::LogEntry& entry) {
590
1
    const auto& http = entry.http();
591
1
    const auto& missing = http.missing_headers();
592
1
    const auto& rejected = http.rejected_headers();
593
1
    return http.rejected_headers_size() == 1 && hasHeader(rejected, "header42") &&
594
1
           http.missing_headers_size() == 1 && hasHeader(missing, "bearer-token", "[redacted]") &&
595
1
           hasHeader(http.headers(), "header42") &&
596
1
           hasHeader(http.headers(), "bearer-token", "d4ef0f5011f163ac");
597
1
  }));
598
1
}
599

            
600
1
TEST_P(CiliumIntegrationTest, AllowedReplacedAndDeleted) {
601
1
  accepted({{":method", "GET"},
602
1
            {":path", "/allowed"},
603
1
            {":authority", "allowedHOST"},
604
1
            {"header42", "anything"}});
605

            
606
  // Validate that missing headers are access logged correctly
607
1
  EXPECT_TRUE(expectAccessLogRequestTo([](const ::cilium::LogEntry& entry) {
608
1
    const auto& http = entry.http();
609
1
    const auto& missing = http.missing_headers();
610
1
    const auto& rejected = http.rejected_headers();
611
1
    return http.rejected_headers_size() == 1 && hasHeader(rejected, "header42") &&
612
1
           http.missing_headers_size() == 2 && hasHeader(missing, "bearer-token", "[redacted]") &&
613
1
           hasHeader(missing, "header2", "value2") && !hasHeader(http.headers(), "header42") &&
614
1
           hasHeader(http.headers(), "header2", "value2") &&
615
1
           hasHeader(http.headers(), "bearer-token", "d4ef0f5011f163ac");
616
1
  }));
617
1
}
618

            
619
1
TEST_P(CiliumIntegrationTest, AllowedHostRegex) {
620
1
  accepted({{":method", "GET"}, {":path", "/maybe/private"}, {":authority", "hostREGEXname"}});
621

            
622
  // Validate that missing headers are access logged correctly
623
1
  EXPECT_TRUE(expectAccessLogRequestTo([](const ::cilium::LogEntry& entry) {
624
1
    const auto& http = entry.http();
625
1
    return http.missing_headers_size() == 0 && http.rejected_headers_size() == 0;
626
1
  }));
627
1
}
628

            
629
1
TEST_P(CiliumIntegrationTest, DeniedMethod) {
630
1
  denied({{":method", "POST"}, {":path", "/maybe/private"}, {":authority", "host"}});
631

            
632
  // Validate that missing headers are access logged correctly
633
1
  EXPECT_TRUE(expectAccessLogDeniedTo([](const ::cilium::LogEntry& entry) {
634
1
    const auto& http = entry.http();
635
1
    return http.missing_headers_size() == 0 && http.rejected_headers_size() == 0;
636
1
  }));
637
1
}
638

            
639
1
TEST_P(CiliumIntegrationTest, AcceptedMethod) {
640
1
  accepted({{":method", "PUT"}, {":path", "/public/opinions"}, {":authority", "host"}});
641

            
642
  // Validate that missing headers are access logged correctly
643
1
  EXPECT_TRUE(expectAccessLogRequestTo([](const ::cilium::LogEntry& entry) {
644
1
    const auto& http = entry.http();
645
1
    return http.missing_headers_size() == 0 && http.rejected_headers_size() == 0;
646
1
  }));
647
1
}
648

            
649
1
TEST_P(CiliumIntegrationTest, L3DeniedPath) {
650
1
  denied({{":method", "GET"}, {":path", "/only-2-allowed"}, {":authority", "host"}});
651

            
652
  // Validate that missing headers are access logged correctly
653
1
  EXPECT_TRUE(expectAccessLogDeniedTo([](const ::cilium::LogEntry& entry) {
654
1
    const auto& http = entry.http();
655
1
    return http.missing_headers_size() == 0 && http.rejected_headers_size() == 0;
656
1
  }));
657
1
}
658

            
659
class CiliumIntegrationEgressTest : public CiliumIntegrationTest {
660
public:
661
  CiliumIntegrationEgressTest()
662
11
      : CiliumIntegrationTest(fmt::format(
663
11
            fmt::runtime(TestEnvironment::substitute(cilium_upstream_config_fmt, GetParam())),
664
11
            "false")) {
665
11
    host_map_config = R"EOF(version_info: "0"
666
11
resources:
667
11
- "@type": type.googleapis.com/cilium.NetworkPolicyHosts
668
11
  policy: 173
669
11
  host_addresses: [ "192.168.0.1", "f00d::1" ]
670
11
- "@type": type.googleapis.com/cilium.NetworkPolicyHosts
671
11
  policy: 1
672
11
  host_addresses: [ "127.0.0.0/8", "::/104" ]
673
11
)EOF";
674
11
  }
675
};
676

            
677
INSTANTIATE_TEST_SUITE_P(IpVersions, CiliumIntegrationEgressTest,
678
                         testing::ValuesIn(TestEnvironment::getIpVersionsForTest()));
679

            
680
1
TEST_P(CiliumIntegrationEgressTest, DeniedPathPrefix) {
681
1
  denied({{":method", "GET"}, {":path", "/prefix"}, {":authority", "host"}});
682
1
}
683

            
684
1
TEST_P(CiliumIntegrationEgressTest, AllowedPathPrefix) {
685
1
  accepted({{":method", "GET"}, {":path", "/allowed"}, {":authority", "host"}});
686
1
}
687

            
688
1
TEST_P(CiliumIntegrationEgressTest, AllowedPathRegex) {
689
1
  accepted({{":method", "GET"}, {":path", "/maybe/public"}, {":authority", "host"}});
690
1
}
691

            
692
1
TEST_P(CiliumIntegrationEgressTest, DeniedPath) {
693
1
  denied({{":method", "GET"}, {":path", "/maybe/private"}, {":authority", "host"}});
694
1
}
695

            
696
1
TEST_P(CiliumIntegrationEgressTest, AllowedHostString) {
697
1
  accepted({{":method", "GET"}, {":path", "/maybe/private"}, {":authority", "allowedHOST"}});
698
1
}
699

            
700
1
TEST_P(CiliumIntegrationEgressTest, AllowedHostRegex) {
701
1
  accepted({{":method", "GET"}, {":path", "/maybe/private"}, {":authority", "hostREGEXname"}});
702
1
}
703

            
704
1
TEST_P(CiliumIntegrationEgressTest, DeniedMethod) {
705
1
  denied({{":method", "POST"}, {":path", "/maybe/private"}, {":authority", "host"}});
706
1
}
707

            
708
1
TEST_P(CiliumIntegrationEgressTest, AcceptedMethod) {
709
1
  accepted({{":method", "PUT"}, {":path", "/public/opinions"}, {":authority", "host"}});
710
1
}
711

            
712
1
TEST_P(CiliumIntegrationEgressTest, L3DeniedPath) {
713
1
  denied({{":method", "GET"}, {":path", "/only-2-allowed"}, {":authority", "host"}});
714
1
}
715

            
716
const std::string L34_POLICY_fmt = R"EOF(version_info: "0"
717
resources:
718
- "@type": type.googleapis.com/cilium.NetworkPolicy
719
  endpoint_ips:
720
  - '{{ ntop_ip_loopback_address }}'
721
  endpoint_id: 3
722
  egress_per_port_policies:
723
  - port: {0}
724
    end_port: {0}
725
    rules:
726
    - remote_policies: [ 42 ]
727
)EOF";
728

            
729
class CiliumIntegrationEgressL34Test : public CiliumIntegrationEgressTest {
730
public:
731
2
  CiliumIntegrationEgressL34Test() = default;
732

            
733
2
  std::string testPolicyFmt() override {
734
2
    return TestEnvironment::substitute(L34_POLICY_fmt, GetParam());
735
2
  }
736
};
737

            
738
INSTANTIATE_TEST_SUITE_P(IpVersions, CiliumIntegrationEgressL34Test,
739
                         testing::ValuesIn(TestEnvironment::getIpVersionsForTest()));
740

            
741
1
TEST_P(CiliumIntegrationEgressL34Test, DeniedPathPrefix) {
742
1
  denied({{":method", "GET"}, {":path", "/prefix"}, {":authority", "host"}});
743
1
}
744

            
745
1
TEST_P(CiliumIntegrationEgressL34Test, DeniedPathPrefix2) {
746
1
  denied({{":method", "GET"}, {":path", "/allowed"}, {":authority", "host"}});
747
1
}
748

            
749
const std::string HEADER_ACTION_MISSING_SDS_POLICY_fmt = R"EOF(version_info: "1"
750
resources:
751
- "@type": type.googleapis.com/cilium.NetworkPolicy
752
  endpoint_ips:
753
  - '{{ ntop_ip_loopback_address }}'
754
  endpoint_id: 3
755
  egress_per_port_policies:
756
  - port: {0}
757
    rules:
758
    - remote_policies: [ 1 ]
759
      http_rules:
760
        http_rules:
761
        - headers:
762
          - name: ':path'
763
            exact_match: '/allowed2'
764
    - remote_policies: [ 42 ]
765
      http_rules:
766
        http_rules:
767
        - headers:
768
          - name: ':path'
769
            exact_match: '/only42'
770
)EOF";
771

            
772
const std::string HEADER_ACTION_MISSING_SDS_POLICY2_fmt = R"EOF(version_info: "2"
773
resources:
774
- "@type": type.googleapis.com/cilium.NetworkPolicy
775
  endpoint_ips:
776
  - '{{ ntop_ip_loopback_address }}'
777
  endpoint_id: 3
778
  egress_per_port_policies:
779
  - port: {0}
780
    rules:
781
    - remote_policies: [ 1 ]
782
      http_rules:
783
        http_rules:
784
        - headers:
785
          - name: ':path'
786
            exact_match: '/allowed'
787
          header_matches:
788
          - name: 'header42'
789
            match_action: FAIL_ON_MATCH
790
            mismatch_action: CONTINUE_ON_MISMATCH
791
          - name: 'bearer-token'
792
            value_sds_secret: 'nonexisting-sds-secret'
793
            mismatch_action: REPLACE_ON_MISMATCH
794
)EOF";
795

            
796
class SDSIntegrationTest : public CiliumIntegrationTest {
797
public:
798
3
  SDSIntegrationTest() {
799
    // switch back to SDS secrets so that we can test with a missing secret.
800
    // File based secret fails if the file does not exist, while SDS should allow for secret to be
801
    // created in future.
802
3
    Cilium::resetSDSConfigFunc();
803

            
804
3
    host_map_config = R"EOF(version_info: "0"
805
3
resources:
806
3
- "@type": type.googleapis.com/cilium.NetworkPolicyHosts
807
3
  policy: 42
808
3
  host_addresses: [ "192.168.1.1", "f00d::1:1" ]
809
3
- "@type": type.googleapis.com/cilium.NetworkPolicyHosts
810
3
  policy: 1
811
3
  host_addresses: [ "127.0.0.0/8", "::/104" ]
812
3
- "@type": type.googleapis.com/cilium.NetworkPolicyHosts
813
3
  policy: 2
814
3
  host_addresses: [ "0.0.0.0/0", "::/0" ]
815
3
)EOF";
816
3
  }
817

            
818
1
  std::string testPolicyFmt2() {
819
1
    return TestEnvironment::substitute(HEADER_ACTION_MISSING_SDS_POLICY2_fmt, GetParam());
820
1
  }
821

            
822
3
  std::string testPolicyFmt() override {
823
3
    return TestEnvironment::substitute(HEADER_ACTION_MISSING_SDS_POLICY_fmt, GetParam());
824
3
  }
825

            
826
3
  std::vector<std::pair<std::string, std::string>> testSecrets() override {
827
3
    return std::vector<std::pair<std::string, std::string>>{}; // No secrets
828
3
  }
829
};
830

            
831
INSTANTIATE_TEST_SUITE_P(IpVersions, SDSIntegrationTest,
832
                         testing::ValuesIn(TestEnvironment::getIpVersionsForTest()));
833

            
834
1
TEST_P(SDSIntegrationTest, TestDeniedL3) {
835
1
  denied({{":method", "GET"}, {":path", "/only42"}, {":authority", "host"}});
836

            
837
  // Validate that missing headers are access logged correctly
838
1
  EXPECT_TRUE(expectAccessLogDeniedTo([](const ::cilium::LogEntry& entry) {
839
1
    auto source_ip = Network::Utility::parseInternetAddressAndPortNoThrow(entry.source_address())
840
1
                         ->ip()
841
1
                         ->addressAsString();
842
1
    const auto& http = entry.http();
843
1
    return http.rejected_headers_size() == 0 && http.missing_headers_size() == 0 &&
844
1
           entry.destination_security_id() == 1 &&
845
1
           source_ip == ((GetParam() == Network::Address::IpVersion::v4) ? "127.0.0.1" : "::1");
846
1
  }));
847
1
}
848

            
849
1
TEST_P(SDSIntegrationTest, TestDeniedL3SpoofedXFF) {
850
1
  denied({{":method", "GET"},
851
1
          {":path", "/only42"},
852
1
          {":authority", "host"},
853
1
          {"x-forwarded-for", "192.168.1.1"}});
854

            
855
  // Validate that missing headers are access logged correctly
856
1
  EXPECT_TRUE(expectAccessLogDeniedTo([](const ::cilium::LogEntry& entry) {
857
1
    auto source_ip = Network::Utility::parseInternetAddressAndPortNoThrow(entry.source_address())
858
1
                         ->ip()
859
1
                         ->addressAsString();
860
1
    const auto& http = entry.http();
861
1
    return http.rejected_headers_size() == 0 && http.missing_headers_size() == 0 &&
862
1
           entry.destination_security_id() == 1 &&
863
1
           source_ip == ((GetParam() == Network::Address::IpVersion::v4) ? "127.0.0.1" : "::1");
864
1
  }));
865
1
}
866

            
867
1
TEST_P(SDSIntegrationTest, TestMissingSDSSecretOnUpdate) {
868
1
  accepted({{":method", "GET"}, {":path", "/allowed2"}, {":authority", "host"}});
869

            
870
  // Validate that missing headers are access logged correctly
871
1
  EXPECT_TRUE(expectAccessLogRequestTo([](const ::cilium::LogEntry& entry) {
872
1
    auto source_ip = Network::Utility::parseInternetAddressAndPortNoThrow(entry.source_address())
873
1
                         ->ip()
874
1
                         ->addressAsString();
875
1
    const auto& http = entry.http();
876
1
    return http.rejected_headers_size() == 0 && http.missing_headers_size() == 0;
877
1
  }));
878

            
879
  // Update policy that still has the missing secret
880
1
  auto port = fake_upstreams_[0]->localAddress()->ip()->port();
881
1
  auto config = fmt::format(fmt::runtime(testPolicyFmt2()), port);
882
1
  std::string temp_path =
883
1
      TestEnvironment::writeStringToFileForTest("network_policy_tmp.yaml", config);
884
1
  std::string backup_path = policy_path + ".backup";
885
1
  TestEnvironment::renameFile(policy_path, backup_path);
886
1
  TestEnvironment::renameFile(temp_path, policy_path);
887
1
  ENVOY_LOG_MISC(debug,
888
1
                 "Updating Cilium Network Policy from file \'{}\'->\'{}\' instead "
889
1
                 "of using gRPC",
890
1
                 temp_path, policy_path);
891

            
892
  // Reduce flakiness by allowing some time for the policy to be updated before the following test
893
1
  absl::SleepFor(absl::Milliseconds(100));
894

            
895
  // 2nd round, on updated policy
896
1
  denied({{":method", "GET"}, {":path", "/allowed"}, {":authority", "host"}});
897

            
898
  // Validate that missing headers are access logged correctly
899
1
  EXPECT_TRUE(expectAccessLogDeniedTo([](const ::cilium::LogEntry& entry) {
900
1
    const auto& http = entry.http();
901
1
    const auto& missing = http.missing_headers();
902
1
    return http.rejected_headers_size() == 0 && http.missing_headers_size() == 1 &&
903
1
           hasHeader(missing, "header42");
904
1
  }));
905

            
906
  // 3rd round back to the initial policy
907
1
  TestEnvironment::renameFile(backup_path, policy_path);
908
1
  ENVOY_LOG_MISC(debug,
909
1
                 "Updating Cilium Network Policy from file \'{}\'->\'{}\' instead "
910
1
                 "of using gRPC",
911
1
                 backup_path, policy_path);
912

            
913
  // Reduce flakiness by allowing some time for the policy to be updated before the following test
914
1
  absl::SleepFor(absl::Milliseconds(100));
915

            
916
1
  denied({{":method", "GET"}, {":path", "/allowed"}, {":authority", "host"}});
917

            
918
  // Validate that missing headers are access logged correctly
919
1
  EXPECT_TRUE(expectAccessLogDeniedTo([](const ::cilium::LogEntry& entry) {
920
1
    const auto& http = entry.http();
921
1
    return http.rejected_headers_size() == 0 && http.missing_headers_size() == 0;
922
1
  }));
923
1
}
924

            
925
} // namespace Envoy