150 Commits

Author SHA1 Message Date
dependabot[bot]
6037c98a77 Bump github.com/prometheus/client_golang from 1.19.0 to 1.19.1 (#181)
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.19.0 to 1.19.1.
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.19.0...v1.19.1)

---
updated-dependencies:
- dependency-name: github.com/prometheus/client_golang
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-03 16:40:23 +01:00
dependabot[bot]
ad237243c4 Bump golang.org/x/crypto from 0.22.0 to 0.25.0 (#188)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.22.0 to 0.25.0.
- [Commits](https://github.com/golang/crypto/compare/v0.22.0...v0.25.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-03 16:39:44 +01:00
Rob Best
1b8a0c3b93 Use custom User-Agent header (#178) 2024-04-28 18:54:55 +01:00
Rob Best
dd2a9a2e71 Add test for TRUSTED CERTIFICATE block (#177)
Not the best test in the world but at least it verifies that we read
this block into a certificate.
2024-04-28 18:16:04 +01:00
MisterVVP
1ec0cd6dc7 fix: support parsing of openssl specific cert formats (#142) 2024-04-28 17:44:52 +01:00
jaroug
515b990f52 Add http_file prober (#144)
* feat: add remote_file probe

* fix: use tls module config

* chore: write http/https tests for probing remote file

* chore: get rid of useless lines

* fix: get rid of useless file download, check body directly

* fix: use checkCertificateMetrics to actually check values

* Rename remote_file to http_file

You can fetch remote content with a lot of different protocols, so I
think it's worth being specific here.

As part of this change I've fixed up some of the logic in the code. I've
also created a separate `http_file` block in the module config.

* Actually include renamed files

---------

Co-authored-by: Anthony LE BERRE <aleberre@veepee.com>
Co-authored-by: Rob Best <rob.best@jetstack.io>
2024-04-28 16:48:09 +01:00
Rob Best
4cb38cb268 Stop running tests twice on PR (#176) 2024-04-28 16:45:53 +01:00
Rob Best
3f34a7b234 Bump modules (#174) 2024-04-28 12:22:42 +01:00
dependabot[bot]
e810f50bea Bump github.com/prometheus/common from 0.48.0 to 0.53.0 (#172)
* Bump github.com/prometheus/common from 0.48.0 to 0.53.0

Bumps [github.com/prometheus/common](https://github.com/prometheus/common) from 0.48.0 to 0.53.0.
- [Release notes](https://github.com/prometheus/common/releases)
- [Commits](https://github.com/prometheus/common/compare/v0.48.0...v0.53.0)

---
updated-dependencies:
- dependency-name: github.com/prometheus/common
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* fix: NewCollector

* chore: bump to Go 1.22

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Rob Best <rob.best@jetstack.io>
2024-04-28 12:08:17 +01:00
dependabot[bot]
ccea4e9ec4 Bump golang.org/x/crypto from 0.21.0 to 0.22.0 (#170)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.21.0 to 0.22.0.
- [Commits](https://github.com/golang/crypto/compare/v0.21.0...v0.22.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-18 15:59:49 +01:00
dependabot[bot]
1cedc5a542 Bump github.com/prometheus/client_model from 0.6.0 to 0.6.1 (#169)
Bumps [github.com/prometheus/client_model](https://github.com/prometheus/client_model) from 0.6.0 to 0.6.1.
- [Release notes](https://github.com/prometheus/client_model/releases)
- [Commits](https://github.com/prometheus/client_model/compare/v0.6.0...v0.6.1)

---
updated-dependencies:
- dependency-name: github.com/prometheus/client_model
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-18 15:59:33 +01:00
Dmitriy Alekseev
3424423d4a Fix tcp starttls test for smtp (#167)
* Fix tcp starttls test for smtp

* Update tcp.go

* Update tcp_test.go

* Update test/tcp.go

Co-authored-by: Rob Best <robertbest89@gmail.com>

* Update tcp_test.go

---------

Co-authored-by: Rob Best <robertbest89@gmail.com>
2024-03-22 12:13:21 +01:00
dependabot[bot]
07786ae452 Bump golang.org/x/crypto from 0.17.0 to 0.21.0 (#165)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.17.0 to 0.21.0.
- [Commits](https://github.com/golang/crypto/compare/v0.17.0...v0.21.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-20 13:52:56 +01:00
dependabot[bot]
734396b9fb Bump github.com/prometheus/client_model from 0.5.0 to 0.6.0 (#161)
Bumps [github.com/prometheus/client_model](https://github.com/prometheus/client_model) from 0.5.0 to 0.6.0.
- [Release notes](https://github.com/prometheus/client_model/releases)
- [Commits](https://github.com/prometheus/client_model/compare/v0.5.0...v0.6.0)

---
updated-dependencies:
- dependency-name: github.com/prometheus/client_model
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-20 13:52:28 +01:00
dependabot[bot]
76e1464643 Bump docker/setup-qemu-action from 2 to 3 (#154)
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 2 to 3.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](https://github.com/docker/setup-qemu-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: docker/setup-qemu-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-20 13:47:33 +01:00
dependabot[bot]
7d95b17e92 Bump actions/checkout from 3 to 4 (#153)
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-20 13:45:09 +01:00
dependabot[bot]
9725277499 Bump docker/login-action from 2 to 3 (#152)
Bumps [docker/login-action](https://github.com/docker/login-action) from 2 to 3.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-20 13:44:28 +01:00
dependabot[bot]
6317311727 Bump actions/upload-artifact from 3 to 4 (#151)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 3 to 4.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-20 13:44:06 +01:00
dependabot[bot]
433631e903 Bump actions/setup-go from 4 to 5 (#150)
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 4 to 5.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-20 13:43:42 +01:00
dependabot[bot]
2e6396494a Bump github.com/prometheus/client_golang from 1.18.0 to 1.19.0 (#166)
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.18.0 to 1.19.0.
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.18.0...v1.19.0)

---
updated-dependencies:
- dependency-name: github.com/prometheus/client_golang
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-20 13:42:34 +01:00
dependabot[bot]
a537a91f4d Bump google.golang.org/protobuf from 1.32.0 to 1.33.0 (#168)
Bumps google.golang.org/protobuf from 1.32.0 to 1.33.0.

---
updated-dependencies:
- dependency-name: google.golang.org/protobuf
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-20 13:41:57 +01:00
wyfrel
376cb7ebba fix description (#155) 2024-03-20 13:30:26 +01:00
dependabot[bot]
890c51077c Bump actions/setup-go from 3 to 4 (#131)
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 3 to 4.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-05 16:55:08 +00:00
Rob Best
3a594bc445 Bump modules and Go version (#148)
* Bump modules and Go version

* Bump go version in github actions
2024-01-05 16:41:58 +00:00
dependabot[bot]
4526fea4ad Bump golang.org/x/text from 0.3.7 to 0.3.8 (#126)
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.3.7 to 0.3.8.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.3.7...v0.3.8)

---
updated-dependencies:
- dependency-name: golang.org/x/text
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-13 00:05:29 +00:00
dependabot[bot]
e8e2ba084c Bump github.com/prometheus/client_model from 0.2.0 to 0.3.0 (#117)
Bumps [github.com/prometheus/client_model](https://github.com/prometheus/client_model) from 0.2.0 to 0.3.0.
- [Release notes](https://github.com/prometheus/client_model/releases)
- [Commits](https://github.com/prometheus/client_model/compare/v0.2.0...v0.3.0)

---
updated-dependencies:
- dependency-name: github.com/prometheus/client_model
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-13 00:04:51 +00:00
dependabot[bot]
9d2fefefef Bump github.com/prometheus/client_golang from 1.12.2 to 1.13.0 (#110)
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.12.2 to 1.13.0.
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.12.2...v1.13.0)

---
updated-dependencies:
- dependency-name: github.com/prometheus/client_golang
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-09-29 13:47:22 +01:00
Rob Best
8b30e0983c Bump dependencies (#108)
* Bump dependencies

* Go 1.18

* go mod tidy
2022-07-15 11:32:47 +01:00
dependabot[bot]
0c3452869d Bump github.com/prometheus/common from 0.35.0 to 0.36.0 (#106)
Bumps [github.com/prometheus/common](https://github.com/prometheus/common) from 0.35.0 to 0.36.0.
- [Release notes](https://github.com/prometheus/common/releases)
- [Commits](https://github.com/prometheus/common/compare/v0.35.0...v0.36.0)

---
updated-dependencies:
- dependency-name: github.com/prometheus/common
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-12 05:48:27 +01:00
dependabot[bot]
cad7f2a3e3 Bump github.com/prometheus/common from 0.34.0 to 0.35.0 (#104)
Bumps [github.com/prometheus/common](https://github.com/prometheus/common) from 0.34.0 to 0.35.0.
- [Release notes](https://github.com/prometheus/common/releases)
- [Commits](https://github.com/prometheus/common/compare/v0.34.0...v0.35.0)

---
updated-dependencies:
- dependency-name: github.com/prometheus/common
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-08 09:54:03 +01:00
dependabot[bot]
57395f8901 Bump github.com/go-kit/log from 0.2.0 to 0.2.1 (#99)
Bumps [github.com/go-kit/log](https://github.com/go-kit/log) from 0.2.0 to 0.2.1.
- [Release notes](https://github.com/go-kit/log/releases)
- [Commits](https://github.com/go-kit/log/compare/v0.2.0...v0.2.1)

---
updated-dependencies:
- dependency-name: github.com/go-kit/log
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-17 06:04:57 +01:00
dependabot[bot]
9c5ba75ff8 Bump docker/setup-qemu-action from 1 to 2 (#96)
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 1 to 2.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](https://github.com/docker/setup-qemu-action/compare/v1...v2)

---
updated-dependencies:
- dependency-name: docker/setup-qemu-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-17 05:35:43 +01:00
dependabot[bot]
8f808b7698 Bump docker/login-action from 1 to 2 (#97)
Bumps [docker/login-action](https://github.com/docker/login-action) from 1 to 2.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v1...v2)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-17 05:35:20 +01:00
dependabot[bot]
120cbe636d Bump github.com/prometheus/client_golang from 1.12.1 to 1.12.2 (#98)
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.12.1 to 1.12.2.
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.12.1...v1.12.2)

---
updated-dependencies:
- dependency-name: github.com/prometheus/client_golang
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-17 05:34:42 +01:00
manishpanjwani21
67a8b2d393 Update README.md (#90)
* Update README.md

Added the Example for type - kubernetes

* Update README.md

Co-authored-by: Rob Best <robertbest89@gmail.com>
2022-05-09 17:56:42 +01:00
Rob Best
52fb44781c Amend module path for v2 2022-05-07 09:33:55 +01:00
dependabot[bot]
793444c203 Bump actions/setup-go from 1 to 3 (#94)
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 1 to 3.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v1...v3)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-06 18:36:25 +01:00
dependabot[bot]
a4b90c67c5 Bump actions/checkout from 2 to 3 (#95)
Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 3.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v2...v3)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-06 18:36:03 +01:00
dependabot[bot]
ee7c7c64de Bump actions/upload-artifact from 2 to 3 (#93)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 2 to 3.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v2...v3)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-06 18:35:46 +01:00
Rob Best
cd7adea7cb Update kubernetes deps 2022-05-05 15:21:54 +01:00
Rob Best
ee51a3ec43 Update go.mod and add dependabot 2022-05-05 14:53:10 +01:00
teuto.net Netzdienste GmbH
65249bc2e7 added pop3 STARTTLS queryResponse (#84)
* added pop3 STARTTLS queryResponse

* implemented pop3 test, added pop3 starttls parameter to README

Co-authored-by: Timo Boldt <tb@teuto.net>
2021-12-31 13:47:05 +00:00
Rob Best
8e318931c5 Build multi-arch docker images 2021-12-31 11:59:20 +00:00
Rob Best
d38244bc39 Release additional archs 2021-12-30 19:39:43 +00:00
Rob Best
02d61835e8 Add default_module and target options
If default_module is set then the exporter will use that when the module
parameter isn't set.

If target is set for a module then the module will use that target,
ignoring the target parameter completely.
2021-12-23 15:28:41 +00:00
Rob Best
087c407585 Move grafana dashboard to contrib
I'm not actively maintaining this and a contrib diredctory indicates
that.
2021-12-23 13:36:54 +00:00
Rob Best
d475f3abd2 Update release instructions 2021-12-23 13:22:10 +00:00
Vegar Sechmann Molvig
a8dcb43b44 Use FieldSelector to select only tls secrets (#82)
This speeds up the listing of certs significatnyly in clusters with many secrets.
2021-12-23 13:18:24 +00:00
Rob Best
0b960631e6 CI improvements 2021-12-23 12:38:02 +00:00
Rob Best
88198bf608 Install goreleaser with go install 2021-12-23 12:33:58 +00:00
Rob Best
b5b2729d01 Go 1.17 and update deps 2021-12-23 12:32:51 +00:00
Ben Ritcey
43dee906c6 Support TLS renegotiation (#83)
* Support TLS renegotiation

* Bump version

* Revert version bump

* Extend TLSConfig with renegotiation support

* Update config/config.go - comment formatting

Co-authored-by: Rob Best <robertbest89@gmail.com>

* add dedicated renegotiation example

* Create local NewTLSConfig in order to incorporate local extentions

* go mod tidy

* Move TLS renegotiation parsing into UnmarshalYAML

Co-authored-by: Rob Best <robertbest89@gmail.com>
2021-12-09 08:34:59 +00:00
Rob Best
78306b97c9 actions: push to Docker Hub 2021-09-11 13:06:47 +01:00
Rob Best
08d9a665b6 Release 2.3.1 2021-08-23 17:44:15 +01:00
Tarvi Pillessaar
a94845ae5d Add support for postgresql protocol (#77)
With postgresql to initiate SSL-encrypted connection specific combination
of bytes must be sent to the server.

Message flow is described on following page
https://www.postgresql.org/docs/13/protocol-flow.html#id-1.10.5.7.11

And SSLRequest message format is described on
https://www.postgresql.org/docs/13/protocol-message-formats.html

The value of SSLRequest message becomes to bytes that is used in the code
2021-08-23 08:39:40 +01:00
Johan Fleury
ef1a35d69f Update dependencies (#76)
* Update dependencies

Fixes #75

* Remove vendor directory
2021-08-07 18:58:33 +01:00
Rob Best
4aaa67e80a Release 2.2.1 2021-06-23 17:28:29 +01:00
Johan Fleury
83f01274fc Move to github.com/prometheus/common/promlog for logging (#71)
* Move to yaml.v3 everywhere

* Switch to github.com/prometheus/common/promlog for logging
2021-06-23 17:22:22 +01:00
Rob Best
d5cbd64f94 Update README.md
- Remove TOC (Github provides one)
- Add quicker links at the top of the doc
2021-05-05 22:48:07 +01:00
treydock
5265251777 Support getting certificate information from a kubeconfig file (#61)
* Support getting certificate information from a kubeconfig file

* Support relative paths for cluster CA and user certificate in kubeconfig

* Determine relative using filepath.IsAbs

* Make relative path logic actually work, add test. Move all kubeconfig parsing into parsing specific function
2021-04-02 10:53:31 +01:00
duchuan
b37574b48f [added] release target add mips64le (#65)
Co-authored-by: duchuanLX <duhchuan@loongson.cn>
2021-02-28 11:00:10 +00:00
Rob Best
5d3ac12e65 release 2.2.0 2020-12-07 20:18:38 +00:00
Rob Best
44d8713091 Add test for TLS version metric 2020-11-19 22:53:47 +00:00
Rob Best
8cde56ce6a Fix examples in the README 2020-11-16 08:47:52 +00:00
Rob Best
fdda9c3eca Add prober column to metrics table 2020-11-16 08:40:48 +00:00
Rob Best
d92d7bed30 Add file prober to example config 2020-11-16 00:49:31 +00:00
Rob Best
ca7aa1f14e Fix golint errors 2020-11-16 00:48:15 +00:00
Rob Best
13a03b1e2b Move tests to prober package 2020-11-16 00:41:36 +00:00
Rob Best
67539b6000 Use same results check for file + kube probes 2020-11-15 22:39:38 +00:00
Rob Best
f4782e3093 Make the description in the README more succinct 2020-11-15 22:28:36 +00:00
Rob Best
63dcb9aff1 Add kubernetes prober 2020-11-15 22:12:18 +00:00
Rob Best
0506638f63 Add file prober 2020-11-15 13:59:51 +00:00
Rob Best
c74c0de901 Refactor prober function and metrics collection
The existing implementation consists of a collector that exports
information from a tls.ConnectionState returned by the prober function.
This won't necessarily integrate well with additional probers that
retrieve certs from sources other than a tls handshake (from file, for
instance).

I've made the probing more generically expandable by removing the
collector and instead registering and collecting metrics inside the
prober. This makes it possible to collect the same metrics in a
different way, or collect different metrics depending on the prober.
2020-11-07 17:17:06 +00:00
Rob Best
e05745b959 Export OCSP stapling metrics (#54)
* Export OCSP stapling metrics

* Add ocsp_response_stapled boolean

* Add missing ocsp_this_update metric to README
2020-10-27 09:10:42 +00:00
Rob Best
896b59b1fe Update deps && go 1.15 2020-10-18 16:48:23 +01:00
Rob Best
119d3cd200 Add a configurable timeout to the module configuration (#55) 2020-10-09 16:47:21 +01:00
Rob Best
224fb62193 release 2.1.1 2020-09-22 18:20:01 +01:00
Rob Best
b84db808b7 Fix ssl_verified_cert_not_after typo 2020-09-22 18:10:07 +01:00
Rob Best
c0f4183a7a release 2.1.0 2020-09-11 19:24:45 +01:00
Rob Best
17aa4e2d2d Add metrics for certificates in the verified chains (#48) 2020-09-11 18:20:41 +01:00
Rob Best
ddedd5f1b5 Add more information to error logs (#49) 2020-09-10 07:35:44 +01:00
Rui Lopes
ac9bc318b1 add a file name extension to the generated binary when the host (e.g. windows) requires it (#47) 2020-09-03 20:02:48 +01:00
Rui Lopes
b4b8471a28 build windows release (#43)
* build windows release

* build a release snapshot
2020-08-19 22:35:49 +01:00
Rob Best
b2ed4e6d4e release 2.0.0 2020-06-22 17:02:48 +01:00
Rob Best
89eff28fac Add starttls for smtp, imap and ftp (#36) 2020-06-22 16:50:21 +01:00
Rob Best
1c8bd16057 Add proxy_url parameter to https configuration (#35)
This supports the use of more than one proxy server per exporter instance.
2020-06-17 16:48:59 +01:00
Rob Best
801179eae7 Move to a modules/probers model, like the blackbox_exporter. (#34)
There are a number of reasons for this change:
- Modules allow a single instance of the exporter to be configured with numerous
different tls configs. Previously you had to run a different exporter for each
combination.
- Adding new and more complicated options to the exporter should be easier with
modules than if I was to go down the route of accepting configuration directly through url params
- I prefer defining a specific prober (https,tcp) over using the URL to guess
what the user wants
2020-06-17 16:29:21 +01:00
Rob Best
5ca5c8ccb9 release 1.0.1 2020-06-16 17:25:09 +01:00
Rob Best
dc2882c1f5 Fix emailAddresses (#33) 2020-06-16 17:23:18 +01:00
Rob Best
8ef058ebfb Modify build and release processes (#32)
- Don't use promu and upstream Prometheus Makefiles. I don't have a clear
process for keeping them in sync with the upstream and I don't think they add
much value.
- Use the same ldflags in goreleaser and the Makefile
- Run goreleaser from the Makefile so custom env vars can be injected
- Update to go 1.14
2020-06-16 17:18:16 +01:00
Rob Best
41830d450f Fix connection leak (#31)
Connections were being left around after requests and in some cases this could
result in file descriptor errors when open files built up.

Closing the http response body and the tcp connection, as well as disabling http
keep alives seems to resolve this.
2020-06-05 13:59:50 +01:00
Rob Best
1305aac408 renew test certificates (#28) 2020-05-18 22:08:15 +01:00
Rob Best
b7cdf62493 update release process notes in README 2020-03-13 10:26:31 +00:00
Rob Best
c98cb10e4f cut 1.0.0 release 2020-03-13 10:14:53 +00:00
Rob Best
66ae153296 add a grafana dashboard (#25) 2020-03-12 18:06:37 +00:00
Rob Best
13519dd2da add goreleaser
This makes cutting a new release infinitely easier
2020-03-12 17:40:03 +00:00
Rob Best
e3477cf63c add TLS version metric (#24) 2020-03-08 18:50:25 +00:00
Rob Best
80765ab97d add a github action to build the docker image 2020-03-08 18:04:43 +00:00
Rob Best
78ce406ce2 fix tests 2020-02-25 08:52:04 +00:00
Rob Best
f81a0d9bc7 1.0.0-rc.0 2020-02-25 08:17:53 +00:00
Rob Best
72736d25c9 Merge pull request #20 from ribbybibby/labels
move metadata out of metrics and into labels
2020-02-25 08:10:08 +00:00
Rob Best
11e3e4c216 move metadata out of metrics and into labels 2020-01-24 17:47:51 +00:00
Rob Best
000c8a8907 add tests for notBefore and notAfter 2020-01-24 17:43:35 +00:00
Rob Best
486b47fd9d describe not before metric 2020-01-24 17:39:12 +00:00
Rob Best
0983ffdba6 use the parsed target when connecting with the http client 2019-12-18 19:26:31 +00:00
Rob Best
874f02f403 fix docker instructions in README 2019-12-18 10:48:50 +00:00
Rob Best
5b927d85bd fix example queries in README 2019-12-08 20:40:43 +00:00
Rob Best
81ff845a10 bump go version in .promu.yaml 2019-12-08 19:59:52 +00:00
Rob Best
008952960e build with go 1.13 explicitly in the Dockerfile 2019-12-08 19:59:33 +00:00
Rob Best
0a4a4023d4 remove unnecessary STATICCHECK_IGNORE from Makefile 2019-12-08 19:58:31 +00:00
Rob Best
6d5223cb4b use promhttp.Handler()
allows the removal of SA1019 linter ignore
2019-12-08 19:33:55 +00:00
Rob Best
0ec420e918 Merge pull request #11 from hans-d/GO-113
make it work with Go 1.13
2019-12-08 19:10:58 +00:00
Hans Donner
81504f6140 make it work with Go 1.13 2019-11-26 19:44:55 +01:00
Rob Best
606f4f6032 0.6.0 2019-09-21 10:40:45 +01:00
Rob Best
f91d97c220 Add TOC to README 2019-09-21 10:28:49 +01:00
Rob Best
cfab972f8f Use https or tcp client based on target address
There are some advantages to using a http client over tcp. For instance,
using http allows you to take advatange of a http proxy, which may be necessary
in some environments.

This commit puts the http client back, alongside tcp, and decides which one to use
based on the target address.
2019-09-21 10:28:49 +01:00
Rob Best
10353fe7fb Merge pull request #7 from ricardbejarano/docker-image
Rewritten Dockerfile into a better image
2019-08-21 12:55:24 +01:00
Ricard Bejarano
5a1b013445 rewritten dockerfile 2019-07-05 15:44:06 +02:00
Rob Best
215029534e Improve tests and remove reliance on external websites 2019-04-10 14:24:18 +01:00
Rob Best
30c8ffb7c3 Fix private key decryption in tests 2019-03-24 20:43:14 +00:00
Rob Best
4c3308f819 Update Makefile and build process to match recent prometheus projects. Address issues thrown up by new tests. 2019-03-24 19:34:32 +00:00
Rob Best
a36358fd5d Fix tests 2019-03-24 18:47:16 +00:00
Rob Best
b65beeb21f Fix subject_ou length check typo 2019-03-22 12:40:51 +00:00
Rob Best
bbc543bc36 Add the compiled binary to .gitignore 2019-03-22 12:10:26 +00:00
Rob Best
f20e251345 Add a few tests around the scheme parsing 2019-03-22 12:10:06 +00:00
Rob Best
be072f46c9 Update version 2019-03-22 12:00:56 +00:00
Rob Best
ba94fb8386 Maintain backwards compatibility by parsing URL scheme into default port values 2019-03-22 12:00:28 +00:00
Rob Best
d1bbe73a08 Update README and examples for tls changes 2019-03-20 18:07:18 +00:00
Rob Best
e45a499637 Support protocols other than HTTPS by reimplementing the exporter as a TLS client 2019-03-20 14:04:24 +00:00
Rob Best
1bb799094c Update VERSION 2019-03-19 13:59:29 +00:00
Rob Best
39c4ef7937 Update and add to tests for client authentication 2019-03-18 12:28:10 +00:00
Rob Best
f17849f9b2 Update documentation for client authentication 2019-03-11 18:43:21 +00:00
Rob Best
7e59584659 Remove the relationship between client authentication and the alternative ca cert bundle.
Client authentication isn't the only situation where you might want to override the system ca bundle with an alternative.
2019-03-11 17:53:21 +00:00
Rob Best
645d7a6e42 Add defaults for the cert and key file flags. This could help reduce the number of flags passed to the exporter if the user is able to adhere to default. 2019-03-11 17:51:29 +00:00
Rob Best
1af3b2a3b2 Add initial support for client authentication 2019-03-11 17:21:22 +00:00
Rob Best
7e95d03166 Some cosmetic fixes
* Add comments to exports
* Update incorrect metric description text
* Rename variables that break typical golang conventions
2019-03-11 17:13:29 +00:00
Rob Best
380d1bf2f9 Merge pull request #2 from tdabasinskas/org
Add support for Organization Unit field
2019-01-25 08:41:15 +00:00
Tomas Dabasinskas
0abd32fac6 Rename ou variable 2019-01-25 10:34:41 +02:00
Tomas Dabasinskas
41450add27 Update docs 2019-01-25 09:56:38 +02:00
Tomas Dabasinskas
b992e2c307 Support organization units 2019-01-25 08:14:56 +02:00
Tomas Dabasinskas
7aafd0d61c Fix the example target 2019-01-25 08:05:17 +02:00
Tomas Dabasinskas
fc34b37f2a Add gitIgnore 2019-01-25 07:58:15 +02:00
Rob Best
1fab18aeb0 Update docker instructions in the README 2017-12-03 12:51:40 +00:00
Rob Best
0763cff093 Increment version 2017-12-03 12:47:26 +00:00
Rob Best
1bc71085ad It looks like there's a standard approach most Prometheus exporters use when building Dockerfiles, so I'm going to adhere to that. Rather than building the binary inside the container, it's built outside and then copied into the bin dir. 2017-12-02 19:03:36 +00:00
Rob Best
a3d8b34adf Add a Makefile that adheres to the Prometheus conventions 2017-12-02 18:55:31 +00:00
Rob Best
c40c4829b2 Add dependencies to a vendor folder, like a good go project does 2017-12-02 18:54:08 +00:00
Rob Best
7572af9013 Adding some basic tests 2017-12-02 15:38:30 +00:00
Rob Best
607378a8c1 Merge pull request #1 from matthope/feature/add_dockerfile
Add Dockerfile
2017-12-02 13:39:18 +00:00
Matt Hope
0774ba2e0a Add Dockerfile 2017-12-02 00:06:37 +11:00
38 changed files with 6085 additions and 253 deletions

11
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,11 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "weekly"

34
.github/workflows/release.yaml vendored Normal file
View File

@@ -0,0 +1,34 @@
name: release
on:
release:
types:
- created
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Unshallow
run: git fetch --prune --unshallow
- name: Set up Qemu
uses: docker/setup-qemu-action@v3
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: 1.22.x
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Release with GoReleaser
run: make release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -0,0 +1,48 @@
name: test-and-snapshot
on:
push:
branches:
- master
pull_request:
branches:
- master
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: 1.22.x
- name: Test
run: make test
snapshot:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Qemu
uses: docker/setup-qemu-action@v3
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: 1.22.x
- name: Build release snapshot
run: make snapshot
- name: Archive release snapshot
uses: actions/upload-artifact@v4
with:
name: release-snapshot
path: |
bin/*.tar.gz
bin/*.txt
bin/*.yaml

17
.gitignore vendored Normal file
View File

@@ -0,0 +1,17 @@
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# KetBrains IDEs files
.idea
ssl_exporter

62
.goreleaser.yaml Normal file
View File

@@ -0,0 +1,62 @@
dist: bin
builds:
- binary: ssl_exporter
env:
- CGO_ENABLED=0
goos:
- linux
- darwin
- windows
goarch:
- "386"
- amd64
- arm
- arm64
- mips64le
flags:
- -v
ldflags: |
-X github.com/prometheus/common/version.Version={{.Version}}
-X github.com/prometheus/common/version.Revision={{.Commit}}
-X github.com/prometheus/common/version.Branch={{.Env.APP_BRANCH}}
-X github.com/prometheus/common/version.BuildUser={{.Env.APP_USER}}@{{.Env.APP_HOST}}
-X github.com/prometheus/common/version.BuildDate={{.Date}}
release:
github:
owner: ribbybibby
name: ssl_exporter
dockers:
- image_templates:
- "{{.Env.APP_DOCKER_IMAGE_NAME}}:{{.Version}}-amd64"
dockerfile: Dockerfile
use: buildx
build_flag_templates:
- "--pull"
- "--label=org.opencontainers.image.created={{.Date}}"
- "--label=org.opencontainers.image.name={{.ProjectName}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=org.opencontainers.image.source={{.GitURL}}"
- "--platform=linux/amd64"
- image_templates:
- "{{.Env.APP_DOCKER_IMAGE_NAME}}:{{.Version}}-arm64"
dockerfile: Dockerfile
use: buildx
build_flag_templates:
- "--pull"
- "--label=org.opencontainers.image.created={{.Date}}"
- "--label=org.opencontainers.image.name={{.ProjectName}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=org.opencontainers.image.source={{.GitURL}}"
- "--platform=linux/arm64"
goarch: arm64
docker_manifests:
- name_template: "{{.Env.APP_DOCKER_IMAGE_NAME}}:{{.Version}}"
image_templates:
- "{{.Env.APP_DOCKER_IMAGE_NAME}}:{{.Version}}-amd64"
- "{{.Env.APP_DOCKER_IMAGE_NAME}}:{{.Version}}-arm64"
- name_template: "{{.Env.APP_DOCKER_IMAGE_NAME}}:latest"
image_templates:
- "{{.Env.APP_DOCKER_IMAGE_NAME}}:{{.Version}}-amd64"
- "{{.Env.APP_DOCKER_IMAGE_NAME}}:{{.Version}}-arm64"

17
Dockerfile Normal file
View File

@@ -0,0 +1,17 @@
FROM alpine:3.15 as build
RUN apk --update add ca-certificates
RUN echo "ssl:*:100:ssl" > /tmp/group && \
echo "ssl:*:100:100::/:/ssl_exporter" > /tmp/passwd
FROM scratch
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
COPY --from=build /tmp/group \
/tmp/passwd \
/etc/
COPY ssl_exporter /
USER ssl:ssl
EXPOSE 9219/tcp
ENTRYPOINT ["/ssl_exporter"]

21
Dockerfile.local Normal file
View File

@@ -0,0 +1,21 @@
FROM golang:1.18-buster AS build
ADD . /tmp/ssl_exporter
RUN cd /tmp/ssl_exporter && \
echo "ssl:*:100:ssl" > group && \
echo "ssl:*:100:100::/:/ssl_exporter" > passwd && \
make
FROM scratch
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=build /tmp/ssl_exporter/group \
/tmp/ssl_exporter/passwd \
/etc/
COPY --from=build /tmp/ssl_exporter/ssl_exporter /
USER ssl:ssl
EXPOSE 9219/tcp
ENTRYPOINT ["/ssl_exporter"]

64
Makefile Normal file
View File

@@ -0,0 +1,64 @@
GOPATH := $(shell go env GOPATH)
BIN_DIR ?= $(shell pwd)/bin
BIN_NAME ?= ssl_exporter$(shell go env GOEXE)
DOCKER_IMAGE_NAME ?= ssl-exporter
DOCKER_IMAGE_TAG ?= $(subst /,-,$(shell git rev-parse --abbrev-ref HEAD))
# Race detector is only supported on amd64.
RACE := $(shell test $$(go env GOARCH) != "amd64" || (echo "-race"))
export APP_HOST ?= $(shell hostname)
export APP_BRANCH ?= $(shell git describe --all --contains --dirty HEAD)
export APP_USER := $(shell id -u --name)
export APP_DOCKER_IMAGE_NAME := ribbybibby/$(DOCKER_IMAGE_NAME)
all: clean format vet build test
style:
@echo ">> checking code style"
@! gofmt -s -d . | grep '^'
test:
@echo ">> running tests"
go test -short -v $(RACE) ./...
format:
@echo ">> formatting code"
@go fmt ./...
vet:
@echo ">> vetting code"
@go vet $(pkgs)
build:
@echo ">> building binary"
@CGO_ENABLED=0 go build -v \
-ldflags "-X github.com/prometheus/common/version.Version=dev \
-X github.com/prometheus/common/version.Revision=$(shell git rev-parse HEAD) \
-X github.com/prometheus/common/version.Branch=$(APP_BRANCH) \
-X github.com/prometheus/common/version.BuildUser=$(APP_USER)@$(APP_HOST) \
-X github.com/prometheus/common/version.BuildDate=$(shell date '+%Y%m%d-%H:%M:%S') \
" \
-o $(BIN_NAME) .
docker:
@echo ">> building docker image"
@docker build -t "$(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG)" -f Dockerfile.local .
$(GOPATH)/bin/goreleaser:
@go install github.com/goreleaser/goreleaser@v1.2.2
snapshot: $(GOPATH)/bin/goreleaser
@echo ">> building snapshot"
@$(GOPATH)/bin/goreleaser --snapshot --skip-sign --skip-validate --skip-publish --rm-dist
release: $(GOPATH)/bin/goreleaser
@$(GOPATH)/bin/goreleaser release
clean:
@echo ">> removing build artifacts"
@rm -Rf $(BIN_DIR)
@rm -Rf $(BIN_NAME)
.PHONY: all style test format vet build docker snapshot release clean

475
README.md
View File

@@ -1,78 +1,459 @@
# SSL Certificate Exporter
The [blackbox_exporter](https://github.com/prometheus/blackbox_exporter) allows you to test the expiry date of a certificate as part of its HTTP(S) probe - which is great. It doesn't, however, tell you which certificate in the chain is nearing expiry or give you any other information that might be useful when sending alerts.
Exports metrics for certificates collected from various sources:
- [TCP probes](#tcp)
- [HTTPS probes](#https)
- [PEM files](#file)
- [Remote PEM files](#http_file)
- [Kubernetes secrets](#kubernetes)
- [Kubeconfig files](#kubeconfig)
For instance, there's a definite value in knowing, upon first receiving an alert, if it's a certificate you manage directly or one further up the chain. It's also not always necessarily clear from the address you're polling what kind of certificate renewal you're looking at. Is it a Let's Encrypt, in which case it should be handled by automation? Or your organisation's wildcard? Maybe the domain is managed by a third-party and you need to submit a ticket to get it renewed.
Whatever it is, the SSL exporter gives you visibility over those dimensions at the point at which you receive an alert. It also allows you to produce more meaningful visualisations and consoles.
The metrics are labelled with fields from the certificate, which allows for
informational dashboards and flexible alert routing.
## Building
go build
./ssl_exporter <flags>
Similarly to the blackbox_exporter, visiting [http://localhost:9219/probe?target=https://example.com](http://localhost:9219/probe?target=https://example.com) will return certificate metrics for example.com. The ```ssl_https_connect_success``` metric indicates if the probe has been successful.
## Flags
./ssl_exporter --help
* __`--tls.insecure`:__ Skip certificate verification (default false). This is insecure but does allow you to collect metrics in the case where a certificate has expired. That being said, I feel that it's more important to catch verification failures than it is to identify an expired certificate, especially as the former includes the latter.
* __`--web.listen-address`:__ The port (default ":9219").
* __`--web.metrics-path`:__ The path metrics are exposed under (default "/metrics")
* __`--web.probe-path`:__ The path the probe endpoint is exposed under (default "/probe")
make
./ssl_exporter <flags>
Similarly to the blackbox_exporter, visiting
[http://localhost:9219/probe?target=example.com:443](http://localhost:9219/probe?target=example.com:443)
will return certificate metrics for example.com. The `ssl_probe_success`
metric indicates if the probe has been successful.
### Docker
docker run -p 9219:9219 ribbybibby/ssl-exporter:latest <flags>
### Release process
- Create a release in Github with a semver tag and GH actions will:
- Add a changelog
- Upload binaries
- Build and push a Docker image
## Usage
```
usage: ssl_exporter [<flags>]
Flags:
-h, --help Show context-sensitive help (also try --help-long and
--help-man).
--web.listen-address=":9219"
Address to listen on for web interface and telemetry.
--web.metrics-path="/metrics"
Path under which to expose metrics
--web.probe-path="/probe" Path under which to expose the probe endpoint
--config.file="" SSL exporter configuration file
--log.level="info" Only log messages with the given severity or above. Valid
levels: [debug, info, warn, error, fatal]
--log.format="logger:stderr"
Set the log target and format. Example:
"logger:syslog?appname=bob&local=7" or
"logger:stdout?json=true"
--version Show application version.
```
## Metrics
Metrics are exported for each certificate in the chain individually. All of the metrics are labelled with the Issuer's Common Name and the Serial ID, which is pretty much a unique identifier.
I considered having a series for each ```ssl_cert_subject_alternative_*``` value but these labels aren't actually very cardinal, considering the most frequently they'll change is probably every three months, which is longer than most metric retention times anyway. Joining them within commas as I've done allows for easy parsing and relabelling.
| Metric | Meaning | Labels | Probers |
| ------------------------------ | ---------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | ---------- |
| ssl_cert_not_after | The date after which a peer certificate expires. Expressed as a Unix Epoch Time. | serial_no, issuer_cn, cn, dnsnames, ips, emails, ou | tcp, https |
| ssl_cert_not_before | The date before which a peer certificate is not valid. Expressed as a Unix Epoch Time. | serial_no, issuer_cn, cn, dnsnames, ips, emails, ou | tcp, https |
| ssl_file_cert_not_after | The date after which a certificate found by the file prober expires. Expressed as a Unix Epoch Time. | file, serial_no, issuer_cn, cn, dnsnames, ips, emails, ou | file |
| ssl_file_cert_not_before | The date before which a certificate found by the file prober is not valid. Expressed as a Unix Epoch Time. | file, serial_no, issuer_cn, cn, dnsnames, ips, emails, ou | file |
| ssl_kubernetes_cert_not_after | The date after which a certificate found by the kubernetes prober expires. Expressed as a Unix Epoch Time. | namespace, secret, key, serial_no, issuer_cn, cn, dnsnames, ips, emails, ou | kubernetes |
| ssl_kubernetes_cert_not_before | The date before which a certificate found by the kubernetes prober is not valid. Expressed as a Unix Epoch Time. | namespace, secret, key, serial_no, issuer_cn, cn, dnsnames, ips, emails, ou | kubernetes |
| ssl_kubeconfig_cert_not_after | The date after which a certificate found by the kubeconfig prober expires. Expressed as a Unix Epoch Time. | kubeconfig, name, type, serial_no, issuer_cn, cn, dnsnames, ips, emails, ou | kubeconfig |
| ssl_kubeconfig_cert_not_before | The date before which a certificate found by the kubeconfig prober is not valid. Expressed as a Unix Epoch Time. | kubeconfig, name, type, serial_no, issuer_cn, cn, dnsnames, ips, emails, ou | kubeconfig |
| ssl_ocsp_response_next_update | The nextUpdate value in the OCSP response. Expressed as a Unix Epoch Time | | tcp, https |
| ssl_ocsp_response_produced_at | The producedAt value in the OCSP response. Expressed as a Unix Epoch Time | | tcp, https |
| ssl_ocsp_response_revoked_at | The revocationTime value in the OCSP response. Expressed as a Unix Epoch Time | | tcp, https |
| ssl_ocsp_response_status | The status in the OCSP response. 0=Good 1=Revoked 2=Unknown | | tcp, https |
| ssl_ocsp_response_stapled | Does the connection state contain a stapled OCSP response? Boolean. | | tcp, https |
| ssl_ocsp_response_this_update | The thisUpdate value in the OCSP response. Expressed as a Unix Epoch Time | | tcp, https |
| ssl_probe_success | Was the probe successful? Boolean. | | all |
| ssl_prober | The prober used by the exporter to connect to the target. Boolean. | prober | all |
| ssl_tls_version_info | The TLS version used. Always 1. | version | tcp, https |
| ssl_verified_cert_not_after | The date after which a certificate in the verified chain expires. Expressed as a Unix Epoch Time. | chain_no, serial_no, issuer_cn, cn, dnsnames, ips, emails, ou | tcp, https |
| ssl_verified_cert_not_before | The date before which a certificate in the verified chain is not valid. Expressed as a Unix Epoch Time. | chain_no, serial_no, issuer_cn, cn, dnsnames, ips, emails, ou | tcp, https |
| Metric | Meaning | Labels |
| ------ | ------- | ------ |
| ssl_cert_not_after | The date after which the certificate expires. Expressed as a Unix Epoch Time. | issuer_cn, serial_no |
| ssl_cert_not_before | The date before which the certificate is not valid. Expressed as a Unix Epoch Time. | issuer_cn, serial_no |
| ssl_cert_subject_common_name | The common name of the certificate. Always has a value of 1 | issuer_cn, serial_no, subject_cn |
| ssl_cert_subject_alternative_dnsnames | The subject alternative names (if any). Always has a value of 1 | issuer_cn, serial_no, dnsnames |
| ssl_cert_subject_alternative_emails | The subject alternative email addresses (if any). Always has a value of 1 | issuer_cn, serial_no, emails |
| ssl_cert_subject_alternative_ips | The subject alternative IP addresses (if any). Always has a value of 1 | issuer_cn, serial_no, ips |
| ssl_https_connect_success | Was the HTTPS connection successful? Boolean. | |
## Configuration
### TCP
Just like with the blackbox_exporter, you should pass the targets to a single
instance of the exporter in a scrape config with a clever bit of relabelling.
This allows you to leverage service discovery and keeps configuration
centralised to your Prometheus config.
## Prometheus
### Configuration
Just like with the blackbox_exporter, you should pass the targets to a single instance of the exporter in a scrape config with a clever bit of relabelling. This allows you to leverage service discovery and keeps configuration centralised to your Prometheus config.
```yml
scrape_configs:
- job_name: 'ssl'
- job_name: "ssl"
metrics_path: /probe
static_configs:
- targets:
- https://example.com
- https://prometheus.io
- example.com:443
- prometheus.io:443
relabel_configs:
- source_labels: [__address__]
target_label: __param_target
- source_labels: [__param_target]
target_label: instance
- target_label: __address__
replacement: 127.0.0.1:9219 # SSL exporter.
replacement: 127.0.0.1:9219 # SSL exporter.
```
### Example Queries
Certificates that expire within 7 days, with Subject Common Name and Subject Alternative Names joined on:
((ssl_cert_not_after - time() < 86400 * 7) * on (instance,issuer_cn,serial_no) group_left (dnsnames) ssl_cert_subject_alternative_dnsnames) * on (instance,issuer_cn,serial_no) group_left (subject_cn) ssl_cert_subject_common_name
### HTTPS
Only return wildcard certificates that are expiring:
((ssl_cert_not_after - time() < 86400 * 7) * on (instance,issuer_cn,serial_no) group_left (subject_cn) ssl_cert_subject_common_name{subject_cn=~"\\*.*"})
By default the exporter will make a TCP connection to the target. This will be
suitable for most cases but if you want to take advantage of http proxying you
can use a HTTPS client by setting the `https` module parameter:
```yml
scrape_configs:
- job_name: "ssl"
metrics_path: /probe
params:
module: ["https"] # <-----
static_configs:
- targets:
- example.com:443
- prometheus.io:443
relabel_configs:
- source_labels: [__address__]
target_label: __param_target
- source_labels: [__param_target]
target_label: instance
- target_label: __address__
replacement: 127.0.0.1:9219
```
Number of certificates in the chain:
count(ssl_cert_subject_common_name) by (instance)
This will use proxy servers discovered by the environment variables `HTTP_PROXY`,
`HTTPS_PROXY` and `ALL_PROXY`. Or, you can set the `https.proxy_url` option in the module
configuration.
Identify instances that have failed to create a valid SSL connection:
The latter takes precedence.
ssl_https_connect_success == 0
### File
## Limitations
I've only exported a subset of the information you could extract from a certificate. It would be simple to add more, for instance organisational information, if there's a need.
The `file` prober exports `ssl_file_cert_not_after` and
`ssl_file_cert_not_before` for PEM encoded certificates found in local files.
## Acknowledgements
The overall structure and implementation of this exporter is based on the [consul_exporter](https://github.com/prometheus/consul_exporter). The probing functionality borrows from the blackbox_exporter.
Files local to the exporter can be scraped by providing them as the target
parameter:
```
curl "localhost:9219/probe?module=file&target=/etc/ssl/cert.pem"
```
The target parameter supports globbing (as provided by the
[doublestar](https://github.com/bmatcuk/doublestar) package),
which allows you to capture multiple files at once:
```
curl "localhost:9219/probe?module=file&target=/etc/ssl/**/*.pem"
```
One specific usage of this prober could be to run the exporter as a DaemonSet in
Kubernetes and then scrape each instance to check the expiry of certificates on
each node:
```yml
scrape_configs:
- job_name: "ssl-kubernetes-file"
metrics_path: /probe
params:
module: ["file"]
target: ["/etc/kubernetes/**/*.crt"]
kubernetes_sd_configs:
- role: node
relabel_configs:
- source_labels: [__address__]
regex: ^(.*):(.*)$
target_label: __address__
replacement: ${1}:9219
```
### HTTP File
The `http_file` prober exports `ssl_cert_not_after` and
`ssl_cert_not_before` for PEM encoded certificates found at the
specified URL.
```
curl "localhost:9219/probe?module=http_file&target=https://www.paypalobjects.com/marketing/web/logos/paypal_com.pem"
```
Here's a sample Prometheus configuration:
```yml
scrape_configs:
- job_name: 'ssl-http-files'
metrics_path: /probe
params:
module: ["http_file"]
static_configs:
- targets:
- 'https://www.paypalobjects.com/marketing/web/logos/paypal_com.pem'
- 'https://d3frv9g52qce38.cloudfront.net/amazondefault/amazon_web_services_inc_2024.pem'
relabel_configs:
- source_labels: [__address__]
target_label: __param_target
- source_labels: [__param_target]
target_label: instance
- target_label: __address__
replacement: 127.0.0.1:9219
```
For proxying to the target resource, this prober will use proxy servers
discovered in the environment variables `HTTP_PROXY`, `HTTPS_PROXY` and
`ALL_PROXY`. Or, you can set the `http_file.proxy_url` option in the module
configuration.
The latter takes precedence.
### Kubernetes
The `kubernetes` prober exports `ssl_kubernetes_cert_not_after` and
`ssl_kubernetes_cert_not_before` for PEM encoded certificates found in secrets
of type `kubernetes.io/tls`.
Provide the namespace and name of the secret in the form `<namespace>/<name>` as
the target:
```
curl "localhost:9219/probe?module=kubernetes&target=kube-system/secret-name"
```
Both the namespace and name portions of the target support glob matching (as provided by the
[doublestar](https://github.com/bmatcuk/doublestar) package):
```
curl "localhost:9219/probe?module=kubernetes&target=kube-system/*"
```
```
curl "localhost:9219/probe?module=kubernetes&target=*/*"
```
The exporter retrieves credentials and context configuration from the following
sources in the following order:
- The `kubeconfig` path in the module configuration
- The `$KUBECONFIG` environment variable
- The default configuration file (`$HOME/.kube/config`)
- The in-cluster environment, if running in a pod
```yml
- job_name: "ssl-kubernetes"
metrics_path: /probe
params:
module: ["kubernetes"]
static_configs:
- targets:
- "test-namespace/nginx-cert"
relabel_configs:
- source_labels: [ __address__ ]
target_label: __param_target
- source_labels: [ __param_target ]
target_label: instance
- target_label: __address__
replacement: 127.0.0.1:9219
```
### Kubeconfig
The `kubeconfig` prober exports `ssl_kubeconfig_cert_not_after` and
`ssl_kubeconfig_cert_not_before` for PEM encoded certificates found in the specified kubeconfig file.
Kubeconfigs local to the exporter can be scraped by providing them as the target
parameter:
```
curl "localhost:9219/probe?module=kubeconfig&target=/etc/kubernetes/admin.conf"
```
One specific usage of this prober could be to run the exporter as a DaemonSet in
Kubernetes and then scrape each instance to check the expiry of certificates on
each node:
```yml
scrape_configs:
- job_name: "ssl-kubernetes-kubeconfig"
metrics_path: /probe
params:
module: ["kubeconfig"]
target: ["/etc/kubernetes/admin.conf"]
kubernetes_sd_configs:
- role: node
relabel_configs:
- source_labels: [__address__]
regex: ^(.*):(.*)$
target_label: __address__
replacement: ${1}:9219
```
## Configuration file
You can provide further module configuration by providing the path to a
configuration file with `--config.file`. The file is written in yaml format,
defined by the schema below.
```
# The default module to use. If omitted, then the module must be provided by the
# 'module' query parameter
default_module: <string>
# Module configuration
modules: [<module>]
```
### \<module\>
```
# The type of probe (https, tcp, file, kubernetes, kubeconfig)
prober: <prober_string>
# The probe target. If set, then the 'target' query parameter is ignored.
# If omitted, then the 'target' query parameter is required.
target: <string>
# How long the probe will wait before giving up.
[ timeout: <duration> ]
# Configuration for TLS
[ tls_config: <tls_config> ]
# The specific probe configuration
[ https: <https_probe> ]
[ tcp: <tcp_probe> ]
[ kubernetes: <kubernetes_probe> ]
[ http_file: <http_file_probe> ]
```
### <tls_config>
```
# Disable target certificate validation.
[ insecure_skip_verify: <boolean> | default = false ]
# Configure TLS renegotiation support.
# Valid options: never, once, freely
[ renegotiation: <string> | default = never ]
# The CA cert to use for the targets.
[ ca_file: <filename> ]
# The client cert file for the targets.
[ cert_file: <filename> ]
# The client key file for the targets.
[ key_file: <filename> ]
# Used to verify the hostname for the targets.
[ server_name: <string> ]
```
### <https_probe>
```
# HTTP proxy server to use to connect to the targets.
[ proxy_url: <string> ]
```
### <tcp_probe>
```
# Use the STARTTLS command before starting TLS for those protocols that support it (smtp, ftp, imap, pop3, postgres)
[ starttls: <string> ]
```
### <kubernetes_probe>
```
# The path of a kubeconfig file to configure the probe
[ kubeconfig: <string> ]
```
### <http_file_probe>
```
# HTTP proxy server to use to connect to the targets.
[ proxy_url: <string> ]
```
## Example Queries
Certificates that expire within 7 days:
```
ssl_cert_not_after - time() < 86400 * 7
```
Wildcard certificates that are expiring:
```
ssl_cert_not_after{cn=~"\*.*"} - time() < 86400 * 7
```
Certificates that expire within 7 days in the verified chain that expires
latest:
```
ssl_verified_cert_not_after{chain_no="0"} - time() < 86400 * 7
```
Number of certificates presented by the server:
```
count(ssl_cert_not_after) by (instance)
```
Identify failed probes:
```
ssl_probe_success == 0
```
## Peer Certificates vs Verified Chain Certificates
Metrics are exported for the `NotAfter` and `NotBefore` fields for peer
certificates as well as for the verified chain that is
constructed by the client.
The former only includes the certificates that are served explicitly by the
target, while the latter can contain multiple chains of trust that are
constructed from root certificates held by the client to the target's server
certificate.
This has important implications when monitoring certificate expiry.
For instance, it may be the case that `ssl_cert_not_after` reports that the root
certificate served by the target is expiring soon even though clients can form
another, much longer lived, chain of trust using another valid root certificate
held locally. In this case, you may want to use `ssl_verified_cert_not_after` to
alert on expiry instead, as this will contain the chain that the client actually
constructs:
```
ssl_verified_cert_not_after{chain_no="0"} - time() < 86400 * 7
```
Each chain is numbered by the exporter in reverse order of expiry, so that
`chain_no="0"` is the chain that will expire the latest. Therefore the query
above will only alert when the chain of trust between the exporter and the
target is truly nearing expiry.
It's very important to note that a query of this kind only represents the chain
of trust between the exporter and the target. Genuine clients may hold different
root certs than the exporter and therefore have different verified chains of
trust.
## Grafana
You can find a simple dashboard [here](contrib/grafana/dashboard.json) that tracks
certificate expiration dates and target connection errors.

172
config/config.go Normal file
View File

@@ -0,0 +1,172 @@
package config
import (
"crypto/tls"
"fmt"
"net/url"
"os"
"time"
pconfig "github.com/prometheus/common/config"
yaml "gopkg.in/yaml.v3"
)
var (
// DefaultConfig is the default configuration that is used when no
// configuration file is provided
DefaultConfig = &Config{
DefaultModule: "tcp",
Modules: map[string]Module{
"tcp": {
Prober: "tcp",
},
"http": {
Prober: "https",
},
"https": {
Prober: "https",
},
"file": {
Prober: "file",
},
"http_file": {
Prober: "http_file",
},
"kubernetes": {
Prober: "kubernetes",
},
"kubeconfig": {
Prober: "kubeconfig",
},
},
}
)
// LoadConfig loads configuration from a file
func LoadConfig(confFile string) (*Config, error) {
var c *Config
yamlReader, err := os.Open(confFile)
if err != nil {
return c, fmt.Errorf("error reading config file: %s", err)
}
defer yamlReader.Close()
decoder := yaml.NewDecoder(yamlReader)
decoder.KnownFields(true)
if err = decoder.Decode(&c); err != nil {
return c, fmt.Errorf("error parsing config file: %s", err)
}
return c, nil
}
// Config configures the exporter
type Config struct {
DefaultModule string `yaml:"default_module"`
Modules map[string]Module `yaml:"modules"`
}
// Module configures a prober
type Module struct {
Prober string `yaml:"prober,omitempty"`
Target string `yaml:"target,omitempty"`
Timeout time.Duration `yaml:"timeout,omitempty"`
TLSConfig TLSConfig `yaml:"tls_config,omitempty"`
HTTPS HTTPSProbe `yaml:"https,omitempty"`
TCP TCPProbe `yaml:"tcp,omitempty"`
Kubernetes KubernetesProbe `yaml:"kubernetes,omitempty"`
HTTPFile HTTPFileProbe `yaml:"http_file,omitempty"`
}
// TLSConfig is a superset of config.TLSConfig that supports TLS renegotiation
type TLSConfig struct {
CAFile string `yaml:"ca_file,omitempty"`
CertFile string `yaml:"cert_file,omitempty"`
KeyFile string `yaml:"key_file,omitempty"`
ServerName string `yaml:"server_name,omitempty"`
InsecureSkipVerify bool `yaml:"insecure_skip_verify"`
// Renegotiation controls what types of TLS renegotiation are supported.
// Supported values: never (default), once, freely.
Renegotiation renegotiation `yaml:"renegotiation,omitempty"`
}
type renegotiation tls.RenegotiationSupport
func (r *renegotiation) UnmarshalYAML(unmarshal func(interface{}) error) error {
var v string
if err := unmarshal(&v); err != nil {
return err
}
switch v {
case "", "never":
*r = renegotiation(tls.RenegotiateNever)
case "once":
*r = renegotiation(tls.RenegotiateOnceAsClient)
case "freely":
*r = renegotiation(tls.RenegotiateFreelyAsClient)
default:
return fmt.Errorf("unsupported TLS renegotiation type %s", v)
}
return nil
}
// NewTLSConfig creates a new tls.Config from the given TLSConfig,
// plus our local extensions
func NewTLSConfig(cfg *TLSConfig) (*tls.Config, error) {
tlsConfig, err := pconfig.NewTLSConfig(&pconfig.TLSConfig{
CAFile: cfg.CAFile,
CertFile: cfg.CertFile,
KeyFile: cfg.KeyFile,
ServerName: cfg.ServerName,
InsecureSkipVerify: cfg.InsecureSkipVerify,
})
if err != nil {
return nil, err
}
tlsConfig.Renegotiation = tls.RenegotiationSupport(cfg.Renegotiation)
return tlsConfig, nil
}
// TCPProbe configures a tcp probe
type TCPProbe struct {
StartTLS string `yaml:"starttls,omitempty"`
}
// HTTPSProbe configures a https probe
type HTTPSProbe struct {
ProxyURL URL `yaml:"proxy_url,omitempty"`
}
// KubernetesProbe configures a kubernetes probe
type KubernetesProbe struct {
Kubeconfig string `yaml:"kubeconfig,omitempty"`
}
// HTTPFileProbe configures a http_file probe
type HTTPFileProbe struct {
ProxyURL URL `yaml:"proxy_url,omitempty"`
}
// URL is a custom URL type that allows validation at configuration load time
type URL struct {
*url.URL
}
// UnmarshalYAML implements the yaml.Unmarshaler interface for URLs.
func (u *URL) UnmarshalYAML(unmarshal func(interface{}) error) error {
var s string
if err := unmarshal(&s); err != nil {
return err
}
urlp, err := url.Parse(s)
if err != nil {
return err
}
u.URL = urlp
return nil
}

View File

@@ -0,0 +1,645 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": "-- Grafana --",
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"description": "Shows certificate expiration times, as well as failed ssl connects",
"editable": true,
"gnetId": 11279,
"graphTooltip": 0,
"id": 2,
"iteration": 1583741464883,
"links": [],
"panels": [
{
"cacheTimeout": null,
"colorBackground": false,
"colorPostfix": false,
"colorPrefix": false,
"colorValue": false,
"colors": ["#299c46", "rgba(237, 129, 40, 0.89)", "#d44a3a"],
"datasource": "Prometheus",
"decimals": 0,
"description": "",
"format": "none",
"gauge": {
"maxValue": 100,
"minValue": 0,
"show": false,
"thresholdLabels": false,
"thresholdMarkers": true
},
"gridPos": {
"h": 7,
"w": 4,
"x": 0,
"y": 0
},
"id": 9,
"interval": null,
"links": [],
"mappingType": 1,
"mappingTypes": [
{
"name": "value to text",
"value": 1
},
{
"name": "range to text",
"value": 2
}
],
"maxDataPoints": 100,
"nullPointMode": "connected",
"nullText": null,
"options": {},
"postfix": "",
"postfixFontSize": "50%",
"prefix": "",
"prefixFontSize": "50%",
"rangeMaps": [
{
"from": "null",
"text": "N/A",
"to": "null"
}
],
"sparkline": {
"fillColor": "rgba(31, 118, 189, 0.18)",
"full": false,
"lineColor": "rgb(31, 120, 193)",
"show": false,
"ymax": null,
"ymin": null
},
"tableColumn": "",
"targets": [
{
"expr": "count(max(ssl_cert_not_after{instance=~\"$instance\",job=~\"$job\"}) by (issuer_cn,serial_no))",
"refId": "A"
}
],
"thresholds": "",
"timeFrom": null,
"timeShift": null,
"title": "Total Unique Certificates",
"transparent": true,
"type": "singlestat",
"valueFontSize": "80%",
"valueMaps": [
{
"op": "=",
"text": "N/A",
"value": "null"
}
],
"valueName": "current"
},
{
"cacheTimeout": null,
"colorBackground": false,
"colorPostfix": false,
"colorPrefix": false,
"colorValue": false,
"colors": ["#299c46", "rgba(237, 129, 40, 0.89)", "#d44a3a"],
"datasource": "Prometheus",
"decimals": 0,
"description": "",
"format": "none",
"gauge": {
"maxValue": 100,
"minValue": 0,
"show": false,
"thresholdLabels": false,
"thresholdMarkers": true
},
"gridPos": {
"h": 7,
"w": 4,
"x": 4,
"y": 0
},
"id": 10,
"interval": null,
"links": [],
"mappingType": 1,
"mappingTypes": [
{
"name": "value to text",
"value": 1
},
{
"name": "range to text",
"value": 2
}
],
"maxDataPoints": 100,
"nullPointMode": "connected",
"nullText": null,
"options": {},
"postfix": "",
"postfixFontSize": "50%",
"prefix": "",
"prefixFontSize": "50%",
"rangeMaps": [
{
"from": "null",
"text": "N/A",
"to": "null"
}
],
"sparkline": {
"fillColor": "rgba(31, 118, 189, 0.18)",
"full": false,
"lineColor": "rgb(31, 120, 193)",
"show": false,
"ymax": null,
"ymin": null
},
"tableColumn": "",
"targets": [
{
"expr": "count(max(ssl_cert_not_after{instance=~\"$instance\",job=~\"$job\"}) by (instance))",
"refId": "A"
}
],
"thresholds": "",
"timeFrom": null,
"timeShift": null,
"title": "Total Probe Targets",
"transparent": true,
"type": "singlestat",
"valueFontSize": "80%",
"valueMaps": [
{
"op": "=",
"text": "N/A",
"value": "null"
}
],
"valueName": "current"
},
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": "Prometheus",
"fill": 0,
"fillGradient": 0,
"gridPos": {
"h": 8,
"w": 14,
"x": 9,
"y": 0
},
"hiddenSeries": false,
"id": 6,
"interval": "",
"legend": {
"alignAsTable": false,
"avg": false,
"current": false,
"hideEmpty": false,
"hideZero": false,
"max": false,
"min": false,
"rightSide": false,
"show": true,
"total": false,
"values": false
},
"lines": false,
"linewidth": 0,
"nullPointMode": "null",
"options": {
"dataLinks": [
{
"targetBlank": true,
"title": "Open URL",
"url": "https://${__field.labels.instance}"
}
]
},
"percentage": false,
"pointradius": 2,
"points": true,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"stack": true,
"steppedLine": false,
"targets": [
{
"expr": "(up{job=~\"$job\", instance=~\"$instance\"} == 0 or ssl_probe_success{job=~\"$job\", instance=~\"$instance\"} == 0)^0",
"format": "time_series",
"instant": false,
"legendFormat": "{{instance}}",
"refId": "A"
}
],
"thresholds": [],
"timeFrom": null,
"timeRegions": [],
"timeShift": null,
"title": "Failed SSL Connects History",
"tooltip": {
"shared": true,
"sort": 1,
"value_type": "individual"
},
"transparent": true,
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"decimals": 0,
"format": "short",
"label": "",
"logBase": 1,
"max": null,
"min": "0",
"show": true
},
{
"decimals": 0,
"format": "short",
"label": "",
"logBase": 1,
"max": null,
"min": "0",
"show": false
}
],
"yaxis": {
"align": false,
"alignLevel": null
}
},
{
"columns": [],
"datasource": "Prometheus",
"description": "Possible reasons:\n- site is down\n- server is down\n- certificate has expired\n- certificate's CA is not trusted by the exporter\n- other connection errors\n- other certificate errors",
"fontSize": "100%",
"gridPos": {
"h": 5,
"w": 24,
"x": 0,
"y": 8
},
"id": 4,
"links": [],
"maxPerRow": 2,
"options": {},
"pageSize": 10,
"repeat": "job",
"repeatDirection": "h",
"scopedVars": {
"job": {
"selected": false,
"text": "ssl",
"value": "ssl"
}
},
"scroll": true,
"showHeader": true,
"sort": {
"col": 2,
"desc": false
},
"styles": [
{
"alias": "Time",
"align": "auto",
"dateFormat": "YYYY-MM-DD HH:mm:ss",
"pattern": "Time",
"type": "hidden"
},
{
"alias": "",
"align": "auto",
"colorMode": null,
"colors": [
"rgba(245, 54, 54, 0.9)",
"rgba(237, 129, 40, 0.89)",
"rgba(50, 172, 45, 0.97)"
],
"decimals": 2,
"pattern": "job",
"thresholds": [],
"type": "hidden",
"unit": "short"
},
{
"alias": "",
"align": "auto",
"colorMode": null,
"colors": [
"rgba(245, 54, 54, 0.9)",
"rgba(237, 129, 40, 0.89)",
"rgba(50, 172, 45, 0.97)"
],
"dateFormat": "YYYY-MM-DD HH:mm:ss",
"decimals": 2,
"mappingType": 1,
"pattern": "__name__",
"thresholds": [],
"type": "hidden",
"unit": "short"
},
{
"alias": "SSL Failed",
"align": "auto",
"colorMode": "row",
"colors": [
"rgba(245, 54, 54, 0.9)",
"rgba(237, 129, 40, 0.89)",
"rgba(50, 172, 45, 0.97)"
],
"dateFormat": "YYYY-MM-DD HH:mm:ss",
"decimals": 0,
"mappingType": 1,
"pattern": "Value",
"thresholds": ["1"],
"type": "number",
"unit": "short"
},
{
"alias": "",
"align": "auto",
"colorMode": "row",
"colors": [
"#F2495C",
"rgba(237, 129, 40, 0.89)",
"rgba(50, 172, 45, 0.97)"
],
"dateFormat": "YYYY-MM-DD HH:mm:ss",
"decimals": 2,
"link": true,
"linkTargetBlank": true,
"linkTooltip": "${__cell}",
"linkUrl": "https://${__cell:raw}",
"mappingType": 1,
"pattern": "instance",
"preserveFormat": false,
"rangeMaps": [],
"sanitize": true,
"thresholds": [""],
"type": "string",
"unit": "short",
"valueMaps": []
}
],
"targets": [
{
"expr": "ssl_probe_success{instance=~\"$instance\",job=~\"$job\"} == 0",
"format": "table",
"instant": true,
"intervalFactor": 1,
"legendFormat": "",
"refId": "A"
}
],
"timeFrom": null,
"timeShift": null,
"title": "Failed SSL Connects - $job",
"transform": "table",
"transparent": true,
"type": "table"
},
{
"cacheTimeout": null,
"columns": [],
"datasource": "Prometheus",
"description": "",
"fontSize": "100%",
"gridPos": {
"h": 25,
"w": 24,
"x": 0,
"y": 18
},
"id": 2,
"interval": "",
"links": [],
"options": {},
"pageSize": null,
"pluginVersion": "6.1.6",
"scroll": true,
"showHeader": true,
"sort": {
"col": 8,
"desc": false
},
"styles": [
{
"alias": "Expires In",
"align": "auto",
"colorMode": "cell",
"colors": [
"rgba(245, 54, 54, 0.9)",
"rgba(237, 129, 40, 0.89)",
"rgba(50, 172, 45, 0.97)"
],
"decimals": 1,
"link": false,
"pattern": "Value",
"thresholds": ["3", "7"],
"type": "number",
"unit": "d"
},
{
"alias": "",
"align": "auto",
"colorMode": null,
"colors": [
"rgba(245, 54, 54, 0.9)",
"rgba(237, 129, 40, 0.89)",
"rgba(50, 172, 45, 0.97)"
],
"dateFormat": "YYYY-MM-DD HH:mm:ss",
"decimals": 2,
"mappingType": 1,
"pattern": "Time",
"thresholds": [],
"type": "hidden",
"unit": "short"
},
{
"alias": "",
"align": "auto",
"colorMode": null,
"colors": [
"rgba(245, 54, 54, 0.9)",
"rgba(237, 129, 40, 0.89)",
"rgba(50, 172, 45, 0.97)"
],
"dateFormat": "YYYY-MM-DD HH:mm:ss",
"decimals": 2,
"mappingType": 1,
"pattern": "job",
"thresholds": [],
"type": "hidden",
"unit": "short"
},
{
"alias": "",
"align": "auto",
"colorMode": null,
"colors": [
"rgba(245, 54, 54, 0.9)",
"rgba(237, 129, 40, 0.89)",
"rgba(50, 172, 45, 0.97)"
],
"dateFormat": "YYYY-MM-DD HH:mm:ss",
"decimals": 2,
"link": true,
"linkTargetBlank": true,
"linkTooltip": "${__cell_6}",
"linkUrl": "https://${__cell:raw}/",
"mappingType": 1,
"pattern": "instance",
"sanitize": false,
"thresholds": [],
"type": "string",
"unit": "short"
},
{
"alias": "",
"align": "auto",
"colorMode": null,
"colors": [
"rgba(245, 54, 54, 0.9)",
"rgba(237, 129, 40, 0.89)",
"rgba(50, 172, 45, 0.97)"
],
"dateFormat": "YYYY-MM-DD HH:mm:ss",
"decimals": 2,
"mappingType": 1,
"pattern": "dnsnames",
"thresholds": [],
"type": "hidden",
"unit": "short"
},
{
"alias": "",
"align": "auto",
"colorMode": null,
"colors": [
"rgba(245, 54, 54, 0.9)",
"rgba(237, 129, 40, 0.89)",
"rgba(50, 172, 45, 0.97)"
],
"dateFormat": "YYYY-MM-DD HH:mm:ss",
"decimals": 2,
"mappingType": 1,
"pattern": "ou",
"thresholds": [],
"type": "hidden",
"unit": "short"
}
],
"targets": [
{
"expr": "((ssl_cert_not_after{instance=~\"$instance\",job=~\"$job\"} - time())/24/60/60)",
"format": "table",
"instant": true,
"interval": "",
"intervalFactor": 1,
"legendFormat": "",
"refId": "A"
}
],
"timeFrom": null,
"timeShift": null,
"title": "SSL Certificates",
"transform": "table",
"type": "table"
}
],
"refresh": "5m",
"schemaVersion": 22,
"style": "dark",
"tags": ["ssl", "tls"],
"templating": {
"list": [
{
"allValue": null,
"current": {
"text": "All",
"value": ["$__all"]
},
"datasource": "Prometheus",
"definition": "label_values(ssl_probe_success, job)",
"hide": 0,
"includeAll": true,
"label": "Job",
"multi": true,
"name": "job",
"options": [],
"query": "label_values(ssl_probe_success, job)",
"refresh": 1,
"regex": "",
"skipUrlSync": false,
"sort": 5,
"tagValuesQuery": "",
"tags": [],
"tagsQuery": "",
"type": "query",
"useTags": false
},
{
"allValue": null,
"current": {
"text": "All",
"value": ["$__all"]
},
"datasource": "Prometheus",
"definition": "label_values({job=~\"$job\"}, instance)",
"hide": 0,
"includeAll": true,
"label": "Instance",
"multi": true,
"name": "instance",
"options": [],
"query": "label_values({job=~\"$job\"}, instance)",
"refresh": 1,
"regex": "",
"skipUrlSync": false,
"sort": 1,
"tagValuesQuery": "",
"tags": [],
"tagsQuery": "",
"type": "query",
"useTags": false
}
]
},
"time": {
"from": "now-1h",
"to": "now"
},
"timepicker": {
"refresh_intervals": ["30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"],
"time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"]
},
"timezone": "browser",
"title": "SSL/TLS Exporter",
"uid": "HyKQlVGWk",
"version": 1
}

View File

@@ -6,15 +6,46 @@ scrape_configs:
metrics_path: /probe
static_configs:
- targets:
- 'https://google.co.uk'
- 'https://prometheus.io'
- 'https://example.com'
- 'https://helloworld.letsencrypt.org'
- 'https://expired.badssl.com'
- 'google.co.uk:443'
- 'prometheus.io:443'
- 'example.com:443'
- 'helloworld.letsencrypt.org:443'
- 'expired.badssl.com:443'
relabel_configs:
- source_labels: [__address__]
target_label: __param_target
- source_labels: [__param_target]
target_label: instance
- target_label: __address__
replacement: 127.0.0.1:9219 # SSL exporter.
replacement: 127.0.0.1:9219 # SSL exporter.
- job_name: 'ssl-files'
metrics_path: /probe
params:
module: ["file"]
target: ["/etc/ssl/**/*.pem"]
static_configs:
- targets:
- 127.0.0.1:9219
- job_name: 'ssl-kubernetes-secrets'
metrics_path: /probe
params:
module: ["kubernetes"]
target: ["kube-system/*"]
static_configs:
- targets:
- 127.0.0.1:9219
- job_name: 'ssl-http-files'
metrics_path: /probe
params:
module: ["http_file"]
static_configs:
- targets:
- 'https://www.paypalobjects.com/marketing/web/logos/paypal_com.pem'
- 'https://d3frv9g52qce38.cloudfront.net/amazondefault/amazon_web_services_inc_2024.pem'
relabel_configs:
- source_labels: [__address__]
target_label: __param_target
- source_labels: [__param_target]
target_label: instance
- target_label: __address__
replacement: 127.0.0.1:9219

View File

@@ -0,0 +1,54 @@
default_module: https
modules:
https:
prober: https
https_insecure:
prober: https
tls_config:
insecure_skip_verify: true
https_renegotiation:
prober: https
tls_config:
renegotiation: freely
https_proxy:
prober: https
https:
proxy_url: "socks5://localhost:8123"
https_timeout:
prober: https
timeout: 3s
tcp:
prober: tcp
tcp_servername:
prober: tcp
tls_config:
server_name: example.com
tcp_client_auth:
prober: tcp
tls_config:
ca_file: /etc/tls/ca.crt
cert_file: /etc/tls/tls.crt
key_file: /etc/tls/tls.key
tcp_smtp_starttls:
prober: tcp
tcp:
starttls: smtp
file:
prober: file
file_ca_certificates:
prober: file
target: /etc/ssl/certs/ca-certificates.crt
http_file:
prober: http_file
http_file_proxy:
prober: http_file
http_file:
proxy_url: "socks5://localhost:8123"
kubernetes:
prober: kubernetes
kubernetes_kubeconfig:
prober: kubernetes
kubernetes:
kubeconfig: /root/.kube/config
kubeconfig:
prober: kubeconfig

72
go.mod Normal file
View File

@@ -0,0 +1,72 @@
module github.com/ribbybibby/ssl_exporter/v2
require (
github.com/alecthomas/kingpin/v2 v2.4.0
github.com/bmatcuk/doublestar/v2 v2.0.4
github.com/go-kit/log v0.2.1
github.com/prometheus/client_golang v1.19.1
github.com/prometheus/client_model v0.6.1
github.com/prometheus/common v0.53.0
golang.org/x/crypto v0.25.0
gopkg.in/yaml.v3 v3.0.1
k8s.io/api v0.30.0
k8s.io/apimachinery v0.30.0
k8s.io/client-go v1.5.2
)
require (
github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/emicklei/go-restful/v3 v3.12.0 // indirect
github.com/evanphx/json-patch v4.12.0+incompatible // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/imdario/mergo v0.3.16 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/jpillora/backoff v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/procfs v0.14.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/xhit/go-str2duration/v2 v2.1.0 // indirect
golang.org/x/net v0.24.0 // indirect
golang.org/x/oauth2 v0.19.0 // indirect
golang.org/x/sys v0.22.0 // indirect
golang.org/x/term v0.22.0 // indirect
golang.org/x/text v0.16.0 // indirect
golang.org/x/time v0.5.0 // indirect
google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
k8s.io/klog/v2 v2.120.1 // indirect
k8s.io/kube-openapi v0.0.0-20240423202451-8948a665c108 // indirect
k8s.io/utils v0.0.0-20240423183400-0849a56e8f22 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
)
replace (
k8s.io/api => k8s.io/api v0.29.0
k8s.io/apimachinery => k8s.io/apimachinery v0.29.0
k8s.io/client-go => k8s.io/client-go v0.29.0
)
go 1.22
toolchain go1.22.1

173
go.sum Normal file
View File

@@ -0,0 +1,173 @@
github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY=
github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE=
github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9 h1:ez/4by2iGztzR4L0zgAOR8lTQK9VlyBVVd7G4omaOQs=
github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bmatcuk/doublestar/v2 v2.0.4 h1:6I6oUiT/sU27eE2OFcWqBhL1SwjyvQuOssxT4a1yidI=
github.com/bmatcuk/doublestar/v2 v2.0.4/go.mod h1:QMmcs3H2AUQICWhfzLXz+IYln8lRQmTZRptLie8RgRw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/emicklei/go-restful/v3 v3.12.0 h1:y2DdzBAURM29NFF94q6RaY4vjIH1rtwDapwQtU84iWk=
github.com/emicklei/go-restful/v3 v3.12.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84=
github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU=
github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 h1:0VpGH+cDhbDtdcweoyCVsF3fhN8kejK6rFe/2FFX2nU=
github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49/go.mod h1:BkkQ4L1KS1xMt2aWSPStnn55ChGC0DPOn2FQYj+f25M=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec=
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4=
github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o=
github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg=
github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.53.0 h1:U2pL9w9nmJwJDa4qqLQ3ZaePJ6ZTwt7cMD3AG3+aLCE=
github.com/prometheus/common v0.53.0/go.mod h1:BrxBKv3FWBIGXw89Mg1AeBq7FSyRzXWI3l3e7W3RN5U=
github.com/prometheus/procfs v0.14.0 h1:Lw4VdGGoKEZilJsayHf0B+9YgLGREba2C6xr+Fdfq6s=
github.com/prometheus/procfs v0.14.0/go.mod h1:XL+Iwz8k8ZabyZfMFHPiilCniixqQarAy5Mu67pHlNQ=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc=
github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/oauth2 v0.19.0 h1:9+E/EZBCbTLNrbN35fHv/a/d/mOBatymz1zbtQrXpIg=
golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
k8s.io/api v0.29.0 h1:NiCdQMY1QOp1H8lfRyeEf8eOwV6+0xA6XEE44ohDX2A=
k8s.io/api v0.29.0/go.mod h1:sdVmXoz2Bo/cb77Pxi71IPTSErEW32xa4aXwKH7gfBA=
k8s.io/apimachinery v0.29.0 h1:+ACVktwyicPz0oc6MTMLwa2Pw3ouLAfAon1wPLtG48o=
k8s.io/apimachinery v0.29.0/go.mod h1:eVBxQ/cwiJxH58eK/jd/vAk4mrxmVlnpBH5J2GbMeis=
k8s.io/client-go v0.29.0 h1:KmlDtFcrdUzOYrBhXHgKw5ycWzc3ryPX5mQe0SkG3y8=
k8s.io/client-go v0.29.0/go.mod h1:yLkXH4HKMAywcrD82KMSmfYg2DlE8mepPR4JGSo5n38=
k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw=
k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20240423202451-8948a665c108 h1:Q8Z7VlGhcJgBHJHYugJ/K/7iB8a2eSxCyxdVjJp+lLY=
k8s.io/kube-openapi v0.0.0-20240423202451-8948a665c108/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98=
k8s.io/utils v0.0.0-20240423183400-0849a56e8f22 h1:ao5hUqGhsqdm+bYbjH/pRkCs0unBGe9UyDahzs9zQzQ=
k8s.io/utils v0.0.0-20240423183400-0849a56e8f22/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4=
sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08=
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=

37
prober/file.go Normal file
View File

@@ -0,0 +1,37 @@
package prober
import (
"context"
"fmt"
"github.com/bmatcuk/doublestar/v2"
"github.com/go-kit/log"
"github.com/prometheus/client_golang/prometheus"
"github.com/ribbybibby/ssl_exporter/v2/config"
)
// ProbeFile collects certificate metrics from local files
func ProbeFile(ctx context.Context, logger log.Logger, target string, module config.Module, registry *prometheus.Registry) error {
errCh := make(chan error, 1)
go func() {
files, err := doublestar.Glob(target)
if err != nil {
errCh <- err
return
}
if len(files) == 0 {
errCh <- fmt.Errorf("No files found")
} else {
errCh <- collectFileMetrics(logger, files, registry)
}
}()
select {
case <-ctx.Done():
return fmt.Errorf("context timeout, ran out of time")
case err := <-errCh:
return err
}
}

200
prober/file_test.go Normal file
View File

@@ -0,0 +1,200 @@
package prober
import (
"context"
"crypto/x509"
"encoding/pem"
"io/ioutil"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/ribbybibby/ssl_exporter/v2/config"
"github.com/ribbybibby/ssl_exporter/v2/test"
"github.com/prometheus/client_golang/prometheus"
)
// TestProbeFile tests a file
func TestProbeFile(t *testing.T) {
cert, certFile, err := createTestFile("", "tls*.crt")
if err != nil {
t.Fatalf(err.Error())
}
defer os.Remove(certFile)
module := config.Module{}
registry := prometheus.NewRegistry()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := ProbeFile(ctx, newTestLogger(), certFile, module, registry); err != nil {
t.Fatalf("error: %s", err)
}
checkFileMetrics(cert, certFile, registry, t)
}
// TestProbeFileGlob tests matching a file with a glob
func TestProbeFileGlob(t *testing.T) {
cert, certFile, err := createTestFile("", "tls*.crt")
if err != nil {
t.Fatalf(err.Error())
}
defer os.Remove(certFile)
module := config.Module{}
registry := prometheus.NewRegistry()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
glob := filepath.Dir(certFile) + "/*.crt"
if err := ProbeFile(ctx, newTestLogger(), glob, module, registry); err != nil {
t.Fatalf("error: %s", err)
}
checkFileMetrics(cert, certFile, registry, t)
}
// TestProbeFileGlobDoubleStar tests matching a file with a ** glob
func TestProbeFileGlobDoubleStar(t *testing.T) {
tmpDir, err := ioutil.TempDir("", "testdir")
if err != nil {
t.Fatalf(err.Error())
}
cert, certFile, err := createTestFile(tmpDir, "tls*.crt")
if err != nil {
t.Fatalf(err.Error())
}
defer os.Remove(certFile)
module := config.Module{}
registry := prometheus.NewRegistry()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
glob := filepath.Dir(filepath.Dir(certFile)) + "/**/*.crt"
if err := ProbeFile(ctx, newTestLogger(), glob, module, registry); err != nil {
t.Fatalf("error: %s", err)
}
checkFileMetrics(cert, certFile, registry, t)
}
// TestProbeFileGlobDoubleStarMultiple tests matching multiple files with a ** glob
func TestProbeFileGlobDoubleStarMultiple(t *testing.T) {
tmpDir, err := ioutil.TempDir("", "testdir")
if err != nil {
t.Fatalf(err.Error())
}
defer os.RemoveAll(tmpDir)
tmpDir1, err := ioutil.TempDir(tmpDir, "testdir")
if err != nil {
t.Fatalf(err.Error())
}
cert1, certFile1, err := createTestFile(tmpDir1, "1*.crt")
if err != nil {
t.Fatalf(err.Error())
}
tmpDir2, err := ioutil.TempDir(tmpDir, "testdir")
if err != nil {
t.Fatalf(err.Error())
}
cert2, certFile2, err := createTestFile(tmpDir2, "2*.crt")
if err != nil {
t.Fatalf(err.Error())
}
module := config.Module{}
registry := prometheus.NewRegistry()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
glob := tmpDir + "/**/*.crt"
if err := ProbeFile(ctx, newTestLogger(), glob, module, registry); err != nil {
t.Fatalf("error: %s", err)
}
checkFileMetrics(cert1, certFile1, registry, t)
checkFileMetrics(cert2, certFile2, registry, t)
}
// Create a certificate and write it to a file
func createTestFile(dir, filename string) (*x509.Certificate, string, error) {
certPEM, _ := test.GenerateTestCertificate(time.Now().Add(time.Hour * 1))
block, _ := pem.Decode([]byte(certPEM))
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, "", err
}
tmpFile, err := ioutil.TempFile(dir, filename)
if err != nil {
return nil, tmpFile.Name(), err
}
if _, err := tmpFile.Write(certPEM); err != nil {
return nil, tmpFile.Name(), err
}
if err := tmpFile.Close(); err != nil {
return nil, tmpFile.Name(), err
}
return cert, tmpFile.Name(), nil
}
// Check metrics
func checkFileMetrics(cert *x509.Certificate, certFile string, registry *prometheus.Registry, t *testing.T) {
mfs, err := registry.Gather()
if err != nil {
t.Fatal(err)
}
ips := ","
for _, ip := range cert.IPAddresses {
ips = ips + ip.String() + ","
}
expectedResults := []*registryResult{
&registryResult{
Name: "ssl_file_cert_not_after",
LabelValues: map[string]string{
"file": certFile,
"serial_no": cert.SerialNumber.String(),
"issuer_cn": cert.Issuer.CommonName,
"cn": cert.Subject.CommonName,
"dnsnames": "," + strings.Join(cert.DNSNames, ",") + ",",
"ips": ips,
"emails": "," + strings.Join(cert.EmailAddresses, ",") + ",",
"ou": "," + strings.Join(cert.Subject.OrganizationalUnit, ",") + ",",
},
Value: float64(cert.NotAfter.Unix()),
},
&registryResult{
Name: "ssl_file_cert_not_before",
LabelValues: map[string]string{
"file": certFile,
"serial_no": cert.SerialNumber.String(),
"issuer_cn": cert.Issuer.CommonName,
"cn": cert.Subject.CommonName,
"dnsnames": "," + strings.Join(cert.DNSNames, ",") + ",",
"ips": ips,
"emails": "," + strings.Join(cert.EmailAddresses, ",") + ",",
"ou": "," + strings.Join(cert.Subject.OrganizationalUnit, ",") + ",",
},
Value: float64(cert.NotBefore.Unix()),
},
}
checkRegistryResults(expectedResults, mfs, t)
}

60
prober/http_file.go Normal file
View File

@@ -0,0 +1,60 @@
package prober
import (
"context"
"fmt"
"io"
"net/http"
"github.com/go-kit/log"
"github.com/prometheus/client_golang/prometheus"
"github.com/ribbybibby/ssl_exporter/v2/config"
)
// ProbeHTTPFile collects certificate metrics from a remote file via http
func ProbeHTTPFile(ctx context.Context, logger log.Logger, target string, module config.Module, registry *prometheus.Registry) error {
proxy := http.ProxyFromEnvironment
if module.HTTPFile.ProxyURL.URL != nil {
proxy = http.ProxyURL(module.HTTPFile.ProxyURL.URL)
}
tlsConfig, err := config.NewTLSConfig(&module.TLSConfig)
if err != nil {
return fmt.Errorf("creating TLS config: %w", err)
}
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: tlsConfig,
Proxy: proxy,
DisableKeepAlives: true,
},
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, target, nil)
if err != nil {
return fmt.Errorf("creating http request: %w", err)
}
req.Header.Set("User-Agent", userAgent)
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("making http request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("unexpected response code: %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("reading response body: %w", err)
}
certs, err := decodeCertificates(body)
if err != nil {
return fmt.Errorf("decoding certificates from response body: %w", err)
}
return collectCertificateMetrics(certs, registry)
}

112
prober/http_file_test.go Normal file
View File

@@ -0,0 +1,112 @@
package prober
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/ribbybibby/ssl_exporter/v2/config"
"github.com/ribbybibby/ssl_exporter/v2/test"
)
func TestProbeHTTPFile(t *testing.T) {
testcertPEM, _ := test.GenerateTestCertificate(time.Now().AddDate(0, 0, 1))
server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write(testcertPEM)
}))
server.Start()
defer server.Close()
registry := prometheus.NewRegistry()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := ProbeHTTPFile(ctx, newTestLogger(), server.URL+"/file", config.Module{}, registry); err != nil {
t.Fatalf("error: %s", err)
}
cert, err := newCertificate(testcertPEM)
if err != nil {
t.Fatal(err)
}
checkCertificateMetrics(cert, registry, t)
}
func TestProbeHTTPFile_NotCertificate(t *testing.T) {
server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("foobar"))
}))
server.Start()
defer server.Close()
registry := prometheus.NewRegistry()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := ProbeHTTPFile(ctx, newTestLogger(), server.URL+"/file", config.Module{}, registry); err == nil {
t.Errorf("expected error but got nil")
}
}
func TestProbeHTTPFile_NotFound(t *testing.T) {
server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
}))
server.Start()
defer server.Close()
registry := prometheus.NewRegistry()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := ProbeHTTPFile(ctx, newTestLogger(), server.URL+"/file", config.Module{}, registry); err == nil {
t.Errorf("expected error but got nil")
}
}
func TestProbeHTTPFileHTTPS(t *testing.T) {
server, certPEM, _, caFile, teardown, err := test.SetupHTTPSServer()
if err != nil {
t.Fatalf(err.Error())
}
defer teardown()
server.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write(certPEM)
})
server.StartTLS()
defer server.Close()
module := config.Module{
TLSConfig: config.TLSConfig{
CAFile: caFile,
InsecureSkipVerify: false,
},
}
registry := prometheus.NewRegistry()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := ProbeHTTPFile(ctx, newTestLogger(), server.URL+"/file", module, registry); err != nil {
t.Fatalf("error: %s", err)
}
cert, err := newCertificate(certPEM)
if err != nil {
t.Fatal(err)
}
checkCertificateMetrics(cert, registry, t)
}

82
prober/https.go Normal file
View File

@@ -0,0 +1,82 @@
package prober
import (
"context"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"strings"
"github.com/go-kit/log"
"github.com/go-kit/log/level"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/version"
"github.com/ribbybibby/ssl_exporter/v2/config"
)
var userAgent = fmt.Sprintf("SSLExporter/%s", version.Version)
// ProbeHTTPS performs a https probe
func ProbeHTTPS(ctx context.Context, logger log.Logger, target string, module config.Module, registry *prometheus.Registry) error {
tlsConfig, err := newTLSConfig("", registry, &module.TLSConfig)
if err != nil {
return err
}
if strings.HasPrefix(target, "http://") {
return fmt.Errorf("Target is using http scheme: %s", target)
}
if !strings.HasPrefix(target, "https://") {
target = "https://" + target
}
targetURL, err := url.Parse(target)
if err != nil {
return err
}
proxy := http.ProxyFromEnvironment
if module.HTTPS.ProxyURL.URL != nil {
proxy = http.ProxyURL(module.HTTPS.ProxyURL.URL)
}
client := &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
Transport: &http.Transport{
TLSClientConfig: tlsConfig,
Proxy: proxy,
DisableKeepAlives: true,
},
}
// Issue a GET request to the target
request, err := http.NewRequest(http.MethodGet, targetURL.String(), nil)
if err != nil {
return err
}
request = request.WithContext(ctx)
request.Header.Set("User-Agent", userAgent)
resp, err := client.Do(request)
if err != nil {
return err
}
defer func() {
_, err := io.Copy(ioutil.Discard, resp.Body)
if err != nil {
level.Error(logger).Log("msg", err)
}
resp.Body.Close()
}()
// Check if the response from the target is encrypted
if resp.TLS == nil {
return fmt.Errorf("The response from %s is unencrypted", targetURL.String())
}
return nil
}

618
prober/https_test.go Normal file
View File

@@ -0,0 +1,618 @@
package prober
import (
"bytes"
"context"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"fmt"
"math/big"
"net/http"
"net/http/httptest"
"net/url"
"os"
"testing"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/ribbybibby/ssl_exporter/v2/config"
"github.com/ribbybibby/ssl_exporter/v2/test"
"golang.org/x/crypto/ocsp"
)
// TestProbeHTTPS tests the typical case
func TestProbeHTTPS(t *testing.T) {
server, certPEM, _, caFile, teardown, err := test.SetupHTTPSServer()
if err != nil {
t.Fatalf(err.Error())
}
defer teardown()
server.StartTLS()
defer server.Close()
module := config.Module{
TLSConfig: config.TLSConfig{
CAFile: caFile,
InsecureSkipVerify: false,
},
}
registry := prometheus.NewRegistry()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := ProbeHTTPS(ctx, newTestLogger(), server.URL, module, registry); err != nil {
t.Fatalf("error: %s", err)
}
cert, err := newCertificate(certPEM)
if err != nil {
t.Fatal(err)
}
checkCertificateMetrics(cert, registry, t)
checkOCSPMetrics([]byte{}, registry, t)
checkTLSVersionMetrics("TLS 1.3", registry, t)
}
// TestProbeHTTPSTimeout tests that the https probe respects the timeout in the
// context
func TestProbeHTTPSTimeout(t *testing.T) {
server, _, _, caFile, teardown, err := test.SetupHTTPSServer()
if err != nil {
t.Fatalf(err.Error())
}
defer teardown()
server.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
time.Sleep(3 * time.Second)
fmt.Fprintln(w, "Hello world")
})
server.StartTLS()
defer server.Close()
module := config.Module{
TLSConfig: config.TLSConfig{
CAFile: caFile,
},
}
registry := prometheus.NewRegistry()
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
if err := ProbeHTTPS(ctx, newTestLogger(), server.URL, module, registry); err == nil {
t.Fatalf("Expected error but returned error was nil")
}
}
// TestProbeHTTPSInvalidName tests hitting the server on an address which isn't
// in the SANs (localhost)
func TestProbeHTTPSInvalidName(t *testing.T) {
server, _, _, caFile, teardown, err := test.SetupHTTPSServer()
if err != nil {
t.Fatalf(err.Error())
}
defer teardown()
server.StartTLS()
defer server.Close()
module := config.Module{
TLSConfig: config.TLSConfig{
CAFile: caFile,
InsecureSkipVerify: false,
},
}
u, err := url.Parse(server.URL)
if err != nil {
t.Fatalf(err.Error())
}
registry := prometheus.NewRegistry()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := ProbeHTTPS(ctx, newTestLogger(), "https://localhost:"+u.Port(), module, registry); err == nil {
t.Fatalf("expected error, but err was nil")
}
}
// TestProbeHTTPSNoScheme tests that the probe is successful when the scheme is
// omitted from the target. The scheme should be added by the prober.
func TestProbeHTTPSNoScheme(t *testing.T) {
server, certPEM, _, caFile, teardown, err := test.SetupHTTPSServer()
if err != nil {
t.Fatalf(err.Error())
}
defer teardown()
server.StartTLS()
defer server.Close()
module := config.Module{
TLSConfig: config.TLSConfig{
CAFile: caFile,
InsecureSkipVerify: false,
},
}
u, err := url.Parse(server.URL)
if err != nil {
t.Fatalf(err.Error())
}
registry := prometheus.NewRegistry()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := ProbeHTTPS(ctx, newTestLogger(), u.Host, module, registry); err != nil {
t.Fatalf("error: %s", err)
}
cert, err := newCertificate(certPEM)
if err != nil {
t.Fatal(err)
}
checkCertificateMetrics(cert, registry, t)
checkOCSPMetrics([]byte{}, registry, t)
checkTLSVersionMetrics("TLS 1.3", registry, t)
}
// TestProbeHTTPSServername tests that the probe is successful when the
// servername is provided in the TLS config
func TestProbeHTTPSServerName(t *testing.T) {
server, certPEM, _, caFile, teardown, err := test.SetupHTTPSServer()
if err != nil {
t.Fatalf(err.Error())
}
defer teardown()
server.StartTLS()
defer server.Close()
u, err := url.Parse(server.URL)
if err != nil {
t.Fatalf(err.Error())
}
module := config.Module{
TLSConfig: config.TLSConfig{
CAFile: caFile,
InsecureSkipVerify: false,
ServerName: u.Hostname(),
},
}
registry := prometheus.NewRegistry()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := ProbeHTTPS(ctx, newTestLogger(), "https://localhost:"+u.Port(), module, registry); err != nil {
t.Fatalf("error: %s", err)
}
cert, err := newCertificate(certPEM)
if err != nil {
t.Fatal(err)
}
checkCertificateMetrics(cert, registry, t)
checkOCSPMetrics([]byte{}, registry, t)
checkTLSVersionMetrics("TLS 1.3", registry, t)
}
// TestProbeHTTPSHTTP tests that the prober fails when hitting a HTTP server
func TestProbeHTTPSHTTP(t *testing.T) {
server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello world")
}))
server.Start()
defer server.Close()
registry := prometheus.NewRegistry()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := ProbeHTTPS(ctx, newTestLogger(), server.URL, config.Module{}, registry); err == nil {
t.Fatalf("expected error, but err was nil")
}
}
// TestProbeHTTPSClientAuth tests that the probe is successful when using client auth
func TestProbeHTTPSClientAuth(t *testing.T) {
server, certPEM, keyPEM, caFile, teardown, err := test.SetupHTTPSServer()
if err != nil {
t.Fatalf(err.Error())
}
defer teardown()
// Configure client auth on the server
certPool := x509.NewCertPool()
certPool.AppendCertsFromPEM(certPEM)
server.TLS.ClientAuth = tls.RequireAndVerifyClientCert
server.TLS.RootCAs = certPool
server.TLS.ClientCAs = certPool
server.StartTLS()
defer server.Close()
// Create cert file
certFile, err := test.WriteFile("cert.pem", certPEM)
if err != nil {
t.Fatalf(err.Error())
}
defer os.Remove(certFile)
// Create key file
keyFile, err := test.WriteFile("key.pem", keyPEM)
if err != nil {
t.Fatalf(err.Error())
}
defer os.Remove(keyFile)
module := config.Module{
TLSConfig: config.TLSConfig{
CAFile: caFile,
CertFile: certFile,
KeyFile: keyFile,
InsecureSkipVerify: false,
},
}
registry := prometheus.NewRegistry()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := ProbeHTTPS(ctx, newTestLogger(), server.URL, module, registry); err != nil {
t.Fatalf("error: %s", err)
}
cert, err := newCertificate(certPEM)
if err != nil {
t.Fatal(err)
}
checkCertificateMetrics(cert, registry, t)
checkOCSPMetrics([]byte{}, registry, t)
checkTLSVersionMetrics("TLS 1.3", registry, t)
}
// TestProbeHTTPSClientAuthWrongClientCert tests that the probe fails with a bad
// client certificate
func TestProbeHTTPSClientAuthWrongClientCert(t *testing.T) {
server, serverCertPEM, _, caFile, teardown, err := test.SetupHTTPSServer()
if err != nil {
t.Fatalf(err.Error())
}
defer teardown()
// Configure client auth on the server
certPool := x509.NewCertPool()
certPool.AppendCertsFromPEM(serverCertPEM)
server.TLS.ClientAuth = tls.RequireAndVerifyClientCert
server.TLS.RootCAs = certPool
server.TLS.ClientCAs = certPool
server.StartTLS()
defer server.Close()
// Create a different cert/key pair that won't be accepted by the server
certPEM, keyPEM := test.GenerateTestCertificate(time.Now().AddDate(0, 0, 1))
// Create cert file
certFile, err := test.WriteFile("cert.pem", certPEM)
if err != nil {
t.Fatalf(err.Error())
}
defer os.Remove(certFile)
// Create key file
keyFile, err := test.WriteFile("key.pem", keyPEM)
if err != nil {
t.Fatalf(err.Error())
}
defer os.Remove(keyFile)
module := config.Module{
TLSConfig: config.TLSConfig{
CAFile: caFile,
CertFile: certFile,
KeyFile: keyFile,
InsecureSkipVerify: false,
},
}
registry := prometheus.NewRegistry()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := ProbeHTTPS(ctx, newTestLogger(), server.URL, module, registry); err == nil {
t.Fatalf("expected error but err is nil")
}
}
// TestProbeHTTPSExpired tests that the probe fails with an expired server cert
func TestProbeHTTPSExpired(t *testing.T) {
server, _, _, caFile, teardown, err := test.SetupHTTPSServer()
if err != nil {
t.Fatalf(err.Error())
}
defer teardown()
// Create a certificate with a notAfter date in the past
certPEM, keyPEM := test.GenerateTestCertificate(time.Now().AddDate(0, 0, -1))
testcert, err := tls.X509KeyPair(certPEM, keyPEM)
if err != nil {
t.Fatalf(err.Error())
}
server.TLS.Certificates = []tls.Certificate{testcert}
server.StartTLS()
defer server.Close()
module := config.Module{
TLSConfig: config.TLSConfig{
CAFile: caFile,
InsecureSkipVerify: false,
},
}
registry := prometheus.NewRegistry()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := ProbeHTTPS(ctx, newTestLogger(), server.URL, module, registry); err == nil {
t.Fatalf("expected error but err is nil")
}
}
// TestProbeHTTPSExpiredInsecure tests that the probe succeeds with an expired server cert
// when skipping cert verification
func TestProbeHTTPSExpiredInsecure(t *testing.T) {
server, certPEM, _, caFile, teardown, err := test.SetupHTTPSServer()
if err != nil {
t.Fatalf(err.Error())
}
defer teardown()
// Create a certificate with a notAfter date in the past
certPEM, keyPEM := test.GenerateTestCertificate(time.Now().AddDate(0, 0, -1))
testcert, err := tls.X509KeyPair(certPEM, keyPEM)
if err != nil {
t.Fatalf(err.Error())
}
server.TLS.Certificates = []tls.Certificate{testcert}
server.StartTLS()
defer server.Close()
module := config.Module{
TLSConfig: config.TLSConfig{
CAFile: caFile,
InsecureSkipVerify: true,
},
}
registry := prometheus.NewRegistry()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := ProbeHTTPS(ctx, newTestLogger(), server.URL, module, registry); err != nil {
t.Fatalf("error: %s", err)
}
cert, err := newCertificate(certPEM)
if err != nil {
t.Fatal(err)
}
checkCertificateMetrics(cert, registry, t)
checkOCSPMetrics([]byte{}, registry, t)
checkTLSVersionMetrics("TLS 1.3", registry, t)
}
// TestProbeHTTPSProxy tests the proxy_url field in the configuration
func TestProbeHTTPSProxy(t *testing.T) {
server, certPEM, _, caFile, teardown, err := test.SetupHTTPSServer()
if err != nil {
t.Fatalf(err.Error())
}
defer teardown()
proxyServer, err := test.SetupHTTPProxyServer()
if err != nil {
t.Fatalf(err.Error())
}
server.StartTLS()
defer server.Close()
proxyServer.Start()
defer proxyServer.Close()
proxyURL, err := url.Parse(proxyServer.URL)
if err != nil {
t.Fatalf(err.Error())
}
badProxyURL, err := url.Parse("http://localhost:6666")
if err != nil {
t.Fatalf(err.Error())
}
module := config.Module{
TLSConfig: config.TLSConfig{
CAFile: caFile,
InsecureSkipVerify: false,
},
HTTPS: config.HTTPSProbe{
// Test with a bad proxy url first
ProxyURL: config.URL{URL: badProxyURL},
},
}
registry := prometheus.NewRegistry()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := ProbeHTTPS(ctx, newTestLogger(), server.URL, module, registry); err == nil {
t.Fatalf("expected error but err was nil")
}
// Test with the proxy url, this shouldn't return an error
module.HTTPS.ProxyURL = config.URL{URL: proxyURL}
if err := ProbeHTTPS(ctx, newTestLogger(), server.URL, module, registry); err != nil {
t.Fatalf("error: %s", err)
}
cert, err := newCertificate(certPEM)
if err != nil {
t.Fatal(err)
}
checkCertificateMetrics(cert, registry, t)
checkOCSPMetrics([]byte{}, registry, t)
checkTLSVersionMetrics("TLS 1.3", registry, t)
}
// TestProbeHTTPSOCSP tests a HTTPS probe with OCSP stapling
func TestProbeHTTPSOCSP(t *testing.T) {
server, certPEM, keyPEM, caFile, teardown, err := test.SetupHTTPSServer()
if err != nil {
t.Fatalf(err.Error())
}
defer teardown()
cert, err := newCertificate(certPEM)
if err != nil {
t.Fatal(err)
}
key, err := newKey(keyPEM)
if err != nil {
t.Fatal(err)
}
resp, err := ocsp.CreateResponse(cert, cert, ocsp.Response{SerialNumber: big.NewInt(64), Status: 1}, key)
if err != nil {
t.Fatalf(err.Error())
}
server.TLS.Certificates[0].OCSPStaple = resp
server.StartTLS()
defer server.Close()
module := config.Module{
TLSConfig: config.TLSConfig{
CAFile: caFile,
},
}
registry := prometheus.NewRegistry()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := ProbeHTTPS(ctx, newTestLogger(), server.URL, module, registry); err != nil {
t.Fatalf("error: %s", err)
}
checkCertificateMetrics(cert, registry, t)
checkOCSPMetrics(resp, registry, t)
checkTLSVersionMetrics("TLS 1.3", registry, t)
}
// TestProbeHTTPSVerifiedChains tests the verified chain metrics returned by a
// https probe
func TestProbeHTTPSVerifiedChains(t *testing.T) {
rootPrivateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatalf(err.Error())
}
rootCertExpiry := time.Now().AddDate(0, 0, 5)
rootCertTmpl := test.GenerateCertificateTemplate(rootCertExpiry)
rootCertTmpl.IsCA = true
rootCertTmpl.SerialNumber = big.NewInt(1)
rootCert, rootCertPem := test.GenerateSelfSignedCertificateWithPrivateKey(rootCertTmpl, rootPrivateKey)
olderRootCertExpiry := time.Now().AddDate(0, 0, 3)
olderRootCertTmpl := test.GenerateCertificateTemplate(olderRootCertExpiry)
olderRootCertTmpl.IsCA = true
olderRootCertTmpl.SerialNumber = big.NewInt(2)
olderRootCert, olderRootCertPem := test.GenerateSelfSignedCertificateWithPrivateKey(olderRootCertTmpl, rootPrivateKey)
oldestRootCertExpiry := time.Now().AddDate(0, 0, 1)
oldestRootCertTmpl := test.GenerateCertificateTemplate(oldestRootCertExpiry)
oldestRootCertTmpl.IsCA = true
oldestRootCertTmpl.SerialNumber = big.NewInt(3)
oldestRootCert, oldestRootCertPem := test.GenerateSelfSignedCertificateWithPrivateKey(oldestRootCertTmpl, rootPrivateKey)
serverCertExpiry := time.Now().AddDate(0, 0, 4)
serverCertTmpl := test.GenerateCertificateTemplate(serverCertExpiry)
serverCertTmpl.SerialNumber = big.NewInt(4)
serverCert, serverCertPem, serverKey := test.GenerateSignedCertificate(serverCertTmpl, olderRootCert, rootPrivateKey)
verifiedChains := [][]*x509.Certificate{
[]*x509.Certificate{
serverCert,
rootCert,
},
[]*x509.Certificate{
serverCert,
olderRootCert,
},
[]*x509.Certificate{
serverCert,
oldestRootCert,
},
}
caCertPem := bytes.Join([][]byte{oldestRootCertPem, olderRootCertPem, rootCertPem}, []byte(""))
server, caFile, teardown, err := test.SetupHTTPSServerWithCertAndKey(
caCertPem,
serverCertPem,
serverKey,
)
if err != nil {
t.Fatalf(err.Error())
}
defer teardown()
server.StartTLS()
defer server.Close()
module := config.Module{
TLSConfig: config.TLSConfig{
CAFile: caFile,
},
}
registry := prometheus.NewRegistry()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := ProbeHTTPS(ctx, newTestLogger(), server.URL, module, registry); err != nil {
t.Fatalf("error: %s", err)
}
checkCertificateMetrics(serverCert, registry, t)
checkOCSPMetrics([]byte{}, registry, t)
checkVerifiedChainMetrics(verifiedChains, registry, t)
checkTLSVersionMetrics("TLS 1.3", registry, t)
}

92
prober/kubeconfig.go Normal file
View File

@@ -0,0 +1,92 @@
package prober
import (
"context"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"github.com/go-kit/log"
"github.com/prometheus/client_golang/prometheus"
"github.com/ribbybibby/ssl_exporter/v2/config"
"gopkg.in/yaml.v3"
)
type KubeConfigCluster struct {
Name string
Cluster KubeConfigClusterCert
}
type KubeConfigClusterCert struct {
CertificateAuthority string `yaml:"certificate-authority"`
CertificateAuthorityData string `yaml:"certificate-authority-data"`
}
type KubeConfigUser struct {
Name string
User KubeConfigUserCert
}
type KubeConfigUserCert struct {
ClientCertificate string `yaml:"client-certificate"`
ClientCertificateData string `yaml:"client-certificate-data"`
}
type KubeConfig struct {
Path string
Clusters []KubeConfigCluster
Users []KubeConfigUser
}
// ProbeKubeconfig collects certificate metrics from kubeconfig files
func ProbeKubeconfig(ctx context.Context, logger log.Logger, target string, module config.Module, registry *prometheus.Registry) error {
if _, err := os.Stat(target); err != nil {
return fmt.Errorf("kubeconfig not found: %s", target)
}
k, err := ParseKubeConfig(target)
if err != nil {
return err
}
err = collectKubeconfigMetrics(logger, *k, registry)
if err != nil {
return err
}
return nil
}
func ParseKubeConfig(file string) (*KubeConfig, error) {
k := &KubeConfig{}
data, err := ioutil.ReadFile(file)
if err != nil {
return nil, err
}
err = yaml.Unmarshal([]byte(data), k)
if err != nil {
return nil, err
}
k.Path = file
clusters := []KubeConfigCluster{}
users := []KubeConfigUser{}
for _, c := range k.Clusters {
// Path is relative to kubeconfig path
if c.Cluster.CertificateAuthority != "" && !filepath.IsAbs(c.Cluster.CertificateAuthority) {
newPath := filepath.Join(filepath.Dir(k.Path), c.Cluster.CertificateAuthority)
c.Cluster.CertificateAuthority = newPath
}
clusters = append(clusters, c)
}
for _, u := range k.Users {
// Path is relative to kubeconfig path
if u.User.ClientCertificate != "" && !filepath.IsAbs(u.User.ClientCertificate) {
newPath := filepath.Join(filepath.Dir(k.Path), u.User.ClientCertificate)
u.User.ClientCertificate = newPath
}
users = append(users, u)
}
k.Clusters = clusters
k.Users = users
return k, nil
}

195
prober/kubeconfig_test.go Normal file
View File

@@ -0,0 +1,195 @@
package prober
import (
"context"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"io/ioutil"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/ribbybibby/ssl_exporter/v2/config"
"github.com/ribbybibby/ssl_exporter/v2/test"
"github.com/prometheus/client_golang/prometheus"
"gopkg.in/yaml.v3"
)
// TestProbeFile tests a file
func TestProbeKubeconfig(t *testing.T) {
cert, kubeconfig, err := createTestKubeconfig("", "kubeconfig")
if err != nil {
t.Fatalf(err.Error())
}
defer os.Remove(kubeconfig)
module := config.Module{}
registry := prometheus.NewRegistry()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := ProbeKubeconfig(ctx, newTestLogger(), kubeconfig, module, registry); err != nil {
t.Fatalf("error: %s", err)
}
checkKubeconfigMetrics(cert, kubeconfig, registry, t)
}
func TestParseKubeConfigRelative(t *testing.T) {
tmpFile, err := ioutil.TempFile("", "kubeconfig")
if err != nil {
t.Fatalf("Unable to create Tempfile: %s", err.Error())
}
defer os.Remove(tmpFile.Name())
file := []byte(`
clusters:
- cluster:
certificate-authority: certs/example/ca.pem
server: https://master.example.com
name: example
users:
- user:
client-certificate: test/ca.pem
name: example`)
if _, err := tmpFile.Write(file); err != nil {
t.Fatalf("Unable to write Tempfile: %s", err.Error())
}
expectedClusterPath := filepath.Join(filepath.Dir(tmpFile.Name()), "certs/example/ca.pem")
expectedUserPath := filepath.Join(filepath.Dir(tmpFile.Name()), "test/ca.pem")
k, err := ParseKubeConfig(tmpFile.Name())
if err != nil {
t.Fatalf("Error parsing kubeconfig: %s", err.Error())
}
if len(k.Clusters) != 1 {
t.Fatalf("Unexpected length for Clusters, got %d", len(k.Clusters))
}
if k.Clusters[0].Cluster.CertificateAuthority != expectedClusterPath {
t.Errorf("Unexpected CertificateAuthority value\nExpected: %s\nGot: %s", expectedClusterPath, k.Clusters[0].Cluster.CertificateAuthority)
}
if len(k.Users) != 1 {
t.Fatalf("Unexpected length for Users, got %d", len(k.Users))
}
if k.Users[0].User.ClientCertificate != expectedUserPath {
t.Errorf("Unexpected ClientCertificate value\nExpected: %s\nGot: %s", expectedUserPath, k.Users[0].User.ClientCertificate)
}
}
// Create a certificate and write it to a file
func createTestKubeconfig(dir, filename string) (*x509.Certificate, string, error) {
certPEM, _ := test.GenerateTestCertificate(time.Now().Add(time.Hour * 1))
clusterCert := KubeConfigClusterCert{CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(certPEM))}
clusters := []KubeConfigCluster{KubeConfigCluster{Name: "kubernetes", Cluster: clusterCert}}
userCert := KubeConfigUserCert{ClientCertificateData: base64.StdEncoding.EncodeToString([]byte(certPEM))}
users := []KubeConfigUser{KubeConfigUser{Name: "kubernetes-admin", User: userCert}}
k := KubeConfig{
Clusters: clusters,
Users: users,
}
block, _ := pem.Decode([]byte(certPEM))
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, "", err
}
tmpFile, err := ioutil.TempFile(dir, filename)
if err != nil {
return nil, tmpFile.Name(), err
}
k.Path = tmpFile.Name()
d, err := yaml.Marshal(&k)
if err != nil {
return nil, tmpFile.Name(), err
}
if _, err := tmpFile.Write(d); err != nil {
return nil, tmpFile.Name(), err
}
if err := tmpFile.Close(); err != nil {
return nil, tmpFile.Name(), err
}
return cert, tmpFile.Name(), nil
}
// Check metrics
func checkKubeconfigMetrics(cert *x509.Certificate, kubeconfig string, registry *prometheus.Registry, t *testing.T) {
mfs, err := registry.Gather()
if err != nil {
t.Fatal(err)
}
ips := ","
for _, ip := range cert.IPAddresses {
ips = ips + ip.String() + ","
}
expectedResults := []*registryResult{
&registryResult{
Name: "ssl_kubeconfig_cert_not_after",
LabelValues: map[string]string{
"kubeconfig": kubeconfig,
"name": "kubernetes",
"type": "cluster",
"serial_no": cert.SerialNumber.String(),
"issuer_cn": cert.Issuer.CommonName,
"cn": cert.Subject.CommonName,
"dnsnames": "," + strings.Join(cert.DNSNames, ",") + ",",
"ips": ips,
"emails": "," + strings.Join(cert.EmailAddresses, ",") + ",",
"ou": "," + strings.Join(cert.Subject.OrganizationalUnit, ",") + ",",
},
Value: float64(cert.NotAfter.Unix()),
},
&registryResult{
Name: "ssl_kubeconfig_cert_not_before",
LabelValues: map[string]string{
"kubeconfig": kubeconfig,
"name": "kubernetes",
"type": "cluster",
"serial_no": cert.SerialNumber.String(),
"issuer_cn": cert.Issuer.CommonName,
"cn": cert.Subject.CommonName,
"dnsnames": "," + strings.Join(cert.DNSNames, ",") + ",",
"ips": ips,
"emails": "," + strings.Join(cert.EmailAddresses, ",") + ",",
"ou": "," + strings.Join(cert.Subject.OrganizationalUnit, ",") + ",",
},
Value: float64(cert.NotBefore.Unix()),
},
&registryResult{
Name: "ssl_kubeconfig_cert_not_after",
LabelValues: map[string]string{
"kubeconfig": kubeconfig,
"name": "kubernetes-admin",
"type": "user",
"serial_no": cert.SerialNumber.String(),
"issuer_cn": cert.Issuer.CommonName,
"cn": cert.Subject.CommonName,
"dnsnames": "," + strings.Join(cert.DNSNames, ",") + ",",
"ips": ips,
"emails": "," + strings.Join(cert.EmailAddresses, ",") + ",",
"ou": "," + strings.Join(cert.Subject.OrganizationalUnit, ",") + ",",
},
Value: float64(cert.NotAfter.Unix()),
},
&registryResult{
Name: "ssl_kubeconfig_cert_not_before",
LabelValues: map[string]string{
"kubeconfig": kubeconfig,
"name": "kubernetes-admin",
"type": "user",
"serial_no": cert.SerialNumber.String(),
"issuer_cn": cert.Issuer.CommonName,
"cn": cert.Subject.CommonName,
"dnsnames": "," + strings.Join(cert.DNSNames, ",") + ",",
"ips": ips,
"emails": "," + strings.Join(cert.EmailAddresses, ",") + ",",
"ou": "," + strings.Join(cert.Subject.OrganizationalUnit, ",") + ",",
},
Value: float64(cert.NotBefore.Unix()),
},
}
checkRegistryResults(expectedResults, mfs, t)
}

86
prober/kubernetes.go Normal file
View File

@@ -0,0 +1,86 @@
package prober
import (
"context"
"fmt"
"strings"
"github.com/bmatcuk/doublestar/v2"
"github.com/go-kit/log"
"github.com/prometheus/client_golang/prometheus"
"github.com/ribbybibby/ssl_exporter/v2/config"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
// Support oidc in kube config files
_ "k8s.io/client-go/plugin/pkg/client/auth/oidc"
)
var (
// ErrKubeBadTarget is returned when the target doesn't match the
// expected form for the kubernetes prober
ErrKubeBadTarget = fmt.Errorf("Target secret must be provided in the form: <namespace>/<name>")
)
// ProbeKubernetes collects certificate metrics from kubernetes.io/tls Secrets
func ProbeKubernetes(ctx context.Context, logger log.Logger, target string, module config.Module, registry *prometheus.Registry) error {
client, err := newKubeClient(module.Kubernetes.Kubeconfig)
if err != nil {
return err
}
return probeKubernetes(ctx, target, module, registry, client)
}
func probeKubernetes(ctx context.Context, target string, module config.Module, registry *prometheus.Registry, client kubernetes.Interface) error {
parts := strings.Split(target, "/")
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
return ErrKubeBadTarget
}
ns := parts[0]
name := parts[1]
var tlsSecrets []v1.Secret
secrets, err := client.CoreV1().Secrets("").List(ctx, metav1.ListOptions{FieldSelector: "type=kubernetes.io/tls"})
if err != nil {
return err
}
for _, secret := range secrets.Items {
nMatch, err := doublestar.Match(ns, secret.Namespace)
if err != nil {
return err
}
sMatch, err := doublestar.Match(name, secret.Name)
if err != nil {
return err
}
if nMatch && sMatch {
tlsSecrets = append(tlsSecrets, secret)
}
}
return collectKubernetesSecretMetrics(tlsSecrets, registry)
}
// newKubeClient returns a Kubernetes client (clientset) from the supplied
// kubeconfig path, the KUBECONFIG environment variable, the default config file
// location ($HOME/.kube/config) or from the in-cluster service account environment.
func newKubeClient(path string) (*kubernetes.Clientset, error) {
loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
if path != "" {
loadingRules.ExplicitPath = path
}
kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
loadingRules,
&clientcmd.ConfigOverrides{},
)
config, err := kubeConfig.ClientConfig()
if err != nil {
return nil, err
}
return kubernetes.NewForConfig(config)
}

190
prober/kubernetes_test.go Normal file
View File

@@ -0,0 +1,190 @@
package prober
import (
"context"
"crypto/x509"
"encoding/pem"
"strings"
"testing"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/ribbybibby/ssl_exporter/v2/config"
"github.com/ribbybibby/ssl_exporter/v2/test"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes/fake"
)
func TestKubernetesProbe(t *testing.T) {
certPEM, _ := test.GenerateTestCertificate(time.Now().Add(time.Hour * 1))
block, _ := pem.Decode([]byte(certPEM))
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
t.Fatal(err)
}
caPEM, _ := test.GenerateTestCertificate(time.Now().Add(time.Hour * 10))
block, _ = pem.Decode([]byte(caPEM))
caCert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
t.Fatal(err)
}
fakeKubeClient := fake.NewSimpleClientset(&v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
Namespace: "bar",
},
Data: map[string][]byte{
"tls.crt": certPEM,
"ca.crt": caPEM,
},
Type: "kubernetes.io/tls",
})
module := config.Module{}
registry := prometheus.NewRegistry()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := probeKubernetes(ctx, "bar/foo", module, registry, fakeKubeClient); err != nil {
t.Fatalf("error: %s", err)
}
checkKubernetesMetrics(cert, "bar", "foo", "tls.crt", registry, t)
checkKubernetesMetrics(caCert, "bar", "foo", "ca.crt", registry, t)
}
func TestKubernetesProbeGlob(t *testing.T) {
certPEM, _ := test.GenerateTestCertificate(time.Now().Add(time.Hour * 1))
block, _ := pem.Decode([]byte(certPEM))
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
t.Fatal(err)
}
caPEM, _ := test.GenerateTestCertificate(time.Now().Add(time.Hour * 10))
block, _ = pem.Decode([]byte(caPEM))
caCert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
t.Fatal(err)
}
certPEM2, _ := test.GenerateTestCertificate(time.Now().Add(time.Hour * 1))
block, _ = pem.Decode([]byte(certPEM2))
cert2, err := x509.ParseCertificate(block.Bytes)
if err != nil {
t.Fatal(err)
}
caPEM2, _ := test.GenerateTestCertificate(time.Now().Add(time.Hour * 10))
block, _ = pem.Decode([]byte(caPEM2))
caCert2, err := x509.ParseCertificate(block.Bytes)
if err != nil {
t.Fatal(err)
}
fakeKubeClient := fake.NewSimpleClientset(&v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
Namespace: "bar",
},
Data: map[string][]byte{
"tls.crt": certPEM,
"ca.crt": caPEM,
},
Type: "kubernetes.io/tls",
},
&v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "fooz",
Namespace: "baz",
},
Data: map[string][]byte{
"tls.crt": certPEM2,
"ca.crt": caPEM2,
},
Type: "kubernetes.io/tls",
})
module := config.Module{}
registry := prometheus.NewRegistry()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := probeKubernetes(ctx, "ba*/*", module, registry, fakeKubeClient); err != nil {
t.Fatalf("error: %s", err)
}
checkKubernetesMetrics(cert, "bar", "foo", "tls.crt", registry, t)
checkKubernetesMetrics(caCert, "bar", "foo", "ca.crt", registry, t)
checkKubernetesMetrics(cert2, "baz", "fooz", "tls.crt", registry, t)
checkKubernetesMetrics(caCert2, "baz", "fooz", "ca.crt", registry, t)
}
func TestKubernetesProbeBadTarget(t *testing.T) {
fakeKubeClient := fake.NewSimpleClientset()
module := config.Module{}
registry := prometheus.NewRegistry()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := probeKubernetes(ctx, "bar/foo/bar", module, registry, fakeKubeClient); err != ErrKubeBadTarget {
t.Fatalf("Expected error: %v, but got %v", ErrKubeBadTarget, err)
}
}
func checkKubernetesMetrics(cert *x509.Certificate, namespace, name, key string, registry *prometheus.Registry, t *testing.T) {
mfs, err := registry.Gather()
if err != nil {
t.Fatal(err)
}
ips := ","
for _, ip := range cert.IPAddresses {
ips = ips + ip.String() + ","
}
expectedResults := []*registryResult{
&registryResult{
Name: "ssl_kubernetes_cert_not_after",
LabelValues: map[string]string{
"namespace": namespace,
"secret": name,
"key": key,
"serial_no": cert.SerialNumber.String(),
"issuer_cn": cert.Issuer.CommonName,
"cn": cert.Subject.CommonName,
"dnsnames": "," + strings.Join(cert.DNSNames, ",") + ",",
"ips": ips,
"emails": "," + strings.Join(cert.EmailAddresses, ",") + ",",
"ou": "," + strings.Join(cert.Subject.OrganizationalUnit, ",") + ",",
},
Value: float64(cert.NotAfter.Unix()),
},
&registryResult{
Name: "ssl_kubernetes_cert_not_before",
LabelValues: map[string]string{
"namespace": namespace,
"secret": name,
"key": key,
"serial_no": cert.SerialNumber.String(),
"issuer_cn": cert.Issuer.CommonName,
"cn": cert.Subject.CommonName,
"dnsnames": "," + strings.Join(cert.DNSNames, ",") + ",",
"ips": ips,
"emails": "," + strings.Join(cert.EmailAddresses, ",") + ",",
"ou": "," + strings.Join(cert.Subject.OrganizationalUnit, ",") + ",",
},
Value: float64(cert.NotBefore.Unix()),
},
}
checkRegistryResults(expectedResults, mfs, t)
}

482
prober/metrics.go Normal file
View File

@@ -0,0 +1,482 @@
package prober
import (
"crypto/tls"
"crypto/x509"
"encoding/base64"
"fmt"
"io/ioutil"
"sort"
"strconv"
"strings"
"time"
"github.com/go-kit/log"
"github.com/go-kit/log/level"
"github.com/prometheus/client_golang/prometheus"
"golang.org/x/crypto/ocsp"
v1 "k8s.io/api/core/v1"
)
const (
namespace = "ssl"
)
func collectConnectionStateMetrics(state tls.ConnectionState, registry *prometheus.Registry) error {
if err := collectTLSVersionMetrics(state.Version, registry); err != nil {
return err
}
if err := collectCertificateMetrics(state.PeerCertificates, registry); err != nil {
return err
}
if err := collectVerifiedChainMetrics(state.VerifiedChains, registry); err != nil {
return err
}
return collectOCSPMetrics(state.OCSPResponse, registry)
}
func collectTLSVersionMetrics(version uint16, registry *prometheus.Registry) error {
var (
tlsVersion = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: prometheus.BuildFQName(namespace, "", "tls_version_info"),
Help: "The TLS version used",
},
[]string{"version"},
)
)
registry.MustRegister(tlsVersion)
var v string
switch version {
case tls.VersionTLS10:
v = "TLS 1.0"
case tls.VersionTLS11:
v = "TLS 1.1"
case tls.VersionTLS12:
v = "TLS 1.2"
case tls.VersionTLS13:
v = "TLS 1.3"
default:
v = "unknown"
}
tlsVersion.WithLabelValues(v).Set(1)
return nil
}
func collectCertificateMetrics(certs []*x509.Certificate, registry *prometheus.Registry) error {
var (
notAfter = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: prometheus.BuildFQName(namespace, "", "cert_not_after"),
Help: "NotAfter expressed as a Unix Epoch Time",
},
[]string{"serial_no", "issuer_cn", "cn", "dnsnames", "ips", "emails", "ou"},
)
notBefore = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: prometheus.BuildFQName(namespace, "", "cert_not_before"),
Help: "NotBefore expressed as a Unix Epoch Time",
},
[]string{"serial_no", "issuer_cn", "cn", "dnsnames", "ips", "emails", "ou"},
)
)
registry.MustRegister(notAfter, notBefore)
certs = uniq(certs)
if len(certs) == 0 {
return fmt.Errorf("No certificates found")
}
for _, cert := range certs {
labels := labelValues(cert)
if !cert.NotAfter.IsZero() {
notAfter.WithLabelValues(labels...).Set(float64(cert.NotAfter.Unix()))
}
if !cert.NotBefore.IsZero() {
notBefore.WithLabelValues(labels...).Set(float64(cert.NotBefore.Unix()))
}
}
return nil
}
func collectVerifiedChainMetrics(verifiedChains [][]*x509.Certificate, registry *prometheus.Registry) error {
var (
verifiedNotAfter = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: prometheus.BuildFQName(namespace, "", "verified_cert_not_after"),
Help: "NotAfter expressed as a Unix Epoch Time",
},
[]string{"chain_no", "serial_no", "issuer_cn", "cn", "dnsnames", "ips", "emails", "ou"},
)
verifiedNotBefore = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: prometheus.BuildFQName(namespace, "", "verified_cert_not_before"),
Help: "NotBefore expressed as a Unix Epoch Time",
},
[]string{"chain_no", "serial_no", "issuer_cn", "cn", "dnsnames", "ips", "emails", "ou"},
)
)
registry.MustRegister(verifiedNotAfter, verifiedNotBefore)
sort.Slice(verifiedChains, func(i, j int) bool {
iExpiry := time.Time{}
for _, cert := range verifiedChains[i] {
if (iExpiry.IsZero() || cert.NotAfter.Before(iExpiry)) && !cert.NotAfter.IsZero() {
iExpiry = cert.NotAfter
}
}
jExpiry := time.Time{}
for _, cert := range verifiedChains[j] {
if (jExpiry.IsZero() || cert.NotAfter.Before(jExpiry)) && !cert.NotAfter.IsZero() {
jExpiry = cert.NotAfter
}
}
return iExpiry.After(jExpiry)
})
for i, chain := range verifiedChains {
chain = uniq(chain)
for _, cert := range chain {
chainNo := strconv.Itoa(i)
labels := append([]string{chainNo}, labelValues(cert)...)
if !cert.NotAfter.IsZero() {
verifiedNotAfter.WithLabelValues(labels...).Set(float64(cert.NotAfter.Unix()))
}
if !cert.NotBefore.IsZero() {
verifiedNotBefore.WithLabelValues(labels...).Set(float64(cert.NotBefore.Unix()))
}
}
}
return nil
}
func collectOCSPMetrics(ocspResponse []byte, registry *prometheus.Registry) error {
var (
ocspStapled = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: prometheus.BuildFQName(namespace, "", "ocsp_response_stapled"),
Help: "If the connection state contains a stapled OCSP response",
},
)
ocspStatus = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: prometheus.BuildFQName(namespace, "", "ocsp_response_status"),
Help: "The status in the OCSP response 0=Good 1=Revoked 2=Unknown",
},
)
ocspProducedAt = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: prometheus.BuildFQName(namespace, "", "ocsp_response_produced_at"),
Help: "The producedAt value in the OCSP response, expressed as a Unix Epoch Time",
},
)
ocspThisUpdate = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: prometheus.BuildFQName(namespace, "", "ocsp_response_this_update"),
Help: "The thisUpdate value in the OCSP response, expressed as a Unix Epoch Time",
},
)
ocspNextUpdate = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: prometheus.BuildFQName(namespace, "", "ocsp_response_next_update"),
Help: "The nextUpdate value in the OCSP response, expressed as a Unix Epoch Time",
},
)
ocspRevokedAt = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: prometheus.BuildFQName(namespace, "", "ocsp_response_revoked_at"),
Help: "The revocationTime value in the OCSP response, expressed as a Unix Epoch Time",
},
)
)
registry.MustRegister(
ocspStapled,
ocspStatus,
ocspProducedAt,
ocspThisUpdate,
ocspNextUpdate,
ocspRevokedAt,
)
if len(ocspResponse) == 0 {
return nil
}
resp, err := ocsp.ParseResponse(ocspResponse, nil)
if err != nil {
return err
}
ocspStapled.Set(1)
ocspStatus.Set(float64(resp.Status))
ocspProducedAt.Set(float64(resp.ProducedAt.Unix()))
ocspThisUpdate.Set(float64(resp.ThisUpdate.Unix()))
ocspNextUpdate.Set(float64(resp.NextUpdate.Unix()))
ocspRevokedAt.Set(float64(resp.RevokedAt.Unix()))
return nil
}
func collectFileMetrics(logger log.Logger, files []string, registry *prometheus.Registry) error {
var (
totalCerts []*x509.Certificate
fileNotAfter = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: prometheus.BuildFQName(namespace, "", "file_cert_not_after"),
Help: "NotAfter expressed as a Unix Epoch Time for a certificate found in a file",
},
[]string{"file", "serial_no", "issuer_cn", "cn", "dnsnames", "ips", "emails", "ou"},
)
fileNotBefore = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: prometheus.BuildFQName(namespace, "", "file_cert_not_before"),
Help: "NotBefore expressed as a Unix Epoch Time for a certificate found in a file",
},
[]string{"file", "serial_no", "issuer_cn", "cn", "dnsnames", "ips", "emails", "ou"},
)
)
registry.MustRegister(fileNotAfter, fileNotBefore)
for _, f := range files {
data, err := ioutil.ReadFile(f)
if err != nil {
level.Debug(logger).Log("msg", fmt.Sprintf("Error reading file %s: %s", f, err))
continue
}
certs, err := decodeCertificates(data)
if err != nil {
return err
}
totalCerts = append(totalCerts, certs...)
for _, cert := range certs {
labels := append([]string{f}, labelValues(cert)...)
if !cert.NotAfter.IsZero() {
fileNotAfter.WithLabelValues(labels...).Set(float64(cert.NotAfter.Unix()))
}
if !cert.NotBefore.IsZero() {
fileNotBefore.WithLabelValues(labels...).Set(float64(cert.NotBefore.Unix()))
}
}
}
if len(totalCerts) == 0 {
return fmt.Errorf("No certificates found")
}
return nil
}
func collectKubernetesSecretMetrics(secrets []v1.Secret, registry *prometheus.Registry) error {
var (
totalCerts []*x509.Certificate
kubernetesNotAfter = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: prometheus.BuildFQName(namespace, "", "kubernetes_cert_not_after"),
Help: "NotAfter expressed as a Unix Epoch Time for a certificate found in a kubernetes secret",
},
[]string{"namespace", "secret", "key", "serial_no", "issuer_cn", "cn", "dnsnames", "ips", "emails", "ou"},
)
kubernetesNotBefore = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: prometheus.BuildFQName(namespace, "", "kubernetes_cert_not_before"),
Help: "NotBefore expressed as a Unix Epoch Time for a certificate found in a kubernetes secret",
},
[]string{"namespace", "secret", "key", "serial_no", "issuer_cn", "cn", "dnsnames", "ips", "emails", "ou"},
)
)
registry.MustRegister(kubernetesNotAfter, kubernetesNotBefore)
for _, secret := range secrets {
for _, key := range []string{"tls.crt", "ca.crt"} {
data := secret.Data[key]
if len(data) == 0 {
continue
}
certs, err := decodeCertificates(data)
if err != nil {
return err
}
totalCerts = append(totalCerts, certs...)
for _, cert := range certs {
labels := append([]string{secret.Namespace, secret.Name, key}, labelValues(cert)...)
if !cert.NotAfter.IsZero() {
kubernetesNotAfter.WithLabelValues(labels...).Set(float64(cert.NotAfter.Unix()))
}
if !cert.NotBefore.IsZero() {
kubernetesNotBefore.WithLabelValues(labels...).Set(float64(cert.NotBefore.Unix()))
}
}
}
}
if len(totalCerts) == 0 {
return fmt.Errorf("No certificates found")
}
return nil
}
func collectKubeconfigMetrics(logger log.Logger, kubeconfig KubeConfig, registry *prometheus.Registry) error {
var (
totalCerts []*x509.Certificate
kubeconfigNotAfter = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: prometheus.BuildFQName(namespace, "kubeconfig", "cert_not_after"),
Help: "NotAfter expressed as a Unix Epoch Time for a certificate found in a kubeconfig",
},
[]string{"kubeconfig", "name", "type", "serial_no", "issuer_cn", "cn", "dnsnames", "ips", "emails", "ou"},
)
kubeconfigNotBefore = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: prometheus.BuildFQName(namespace, "kubeconfig", "cert_not_before"),
Help: "NotBefore expressed as a Unix Epoch Time for a certificate found in a kubeconfig",
},
[]string{"kubeconfig", "name", "type", "serial_no", "issuer_cn", "cn", "dnsnames", "ips", "emails", "ou"},
)
)
registry.MustRegister(kubeconfigNotAfter, kubeconfigNotBefore)
for _, c := range kubeconfig.Clusters {
var data []byte
var err error
if c.Cluster.CertificateAuthorityData != "" {
data, err = base64.StdEncoding.DecodeString(c.Cluster.CertificateAuthorityData)
if err != nil {
return err
}
} else if c.Cluster.CertificateAuthority != "" {
data, err = ioutil.ReadFile(c.Cluster.CertificateAuthority)
if err != nil {
level.Debug(logger).Log("msg", fmt.Sprintf("Error reading file %s: %s", c.Cluster.CertificateAuthority, err))
return err
}
}
if data == nil {
continue
}
certs, err := decodeCertificates(data)
if err != nil {
return err
}
totalCerts = append(totalCerts, certs...)
for _, cert := range certs {
labels := append([]string{kubeconfig.Path, c.Name, "cluster"}, labelValues(cert)...)
if !cert.NotAfter.IsZero() {
kubeconfigNotAfter.WithLabelValues(labels...).Set(float64(cert.NotAfter.Unix()))
}
if !cert.NotBefore.IsZero() {
kubeconfigNotBefore.WithLabelValues(labels...).Set(float64(cert.NotBefore.Unix()))
}
}
}
for _, u := range kubeconfig.Users {
var data []byte
var err error
if u.User.ClientCertificateData != "" {
data, err = base64.StdEncoding.DecodeString(u.User.ClientCertificateData)
if err != nil {
return err
}
} else if u.User.ClientCertificate != "" {
data, err = ioutil.ReadFile(u.User.ClientCertificate)
if err != nil {
level.Debug(logger).Log("msg", fmt.Sprintf("Error reading file %s: %s", u.User.ClientCertificate, err))
return err
}
}
if data == nil {
continue
}
certs, err := decodeCertificates(data)
if err != nil {
return err
}
totalCerts = append(totalCerts, certs...)
for _, cert := range certs {
labels := append([]string{kubeconfig.Path, u.Name, "user"}, labelValues(cert)...)
if !cert.NotAfter.IsZero() {
kubeconfigNotAfter.WithLabelValues(labels...).Set(float64(cert.NotAfter.Unix()))
}
if !cert.NotBefore.IsZero() {
kubeconfigNotBefore.WithLabelValues(labels...).Set(float64(cert.NotBefore.Unix()))
}
}
}
if len(totalCerts) == 0 {
return fmt.Errorf("No certificates found")
}
return nil
}
func labelValues(cert *x509.Certificate) []string {
return []string{
cert.SerialNumber.String(),
cert.Issuer.CommonName,
cert.Subject.CommonName,
dnsNames(cert),
ipAddresses(cert),
emailAddresses(cert),
organizationalUnits(cert),
}
}
func dnsNames(cert *x509.Certificate) string {
if len(cert.DNSNames) > 0 {
return "," + strings.Join(cert.DNSNames, ",") + ","
}
return ""
}
func emailAddresses(cert *x509.Certificate) string {
if len(cert.EmailAddresses) > 0 {
return "," + strings.Join(cert.EmailAddresses, ",") + ","
}
return ""
}
func ipAddresses(cert *x509.Certificate) string {
if len(cert.IPAddresses) > 0 {
ips := ","
for _, ip := range cert.IPAddresses {
ips = ips + ip.String() + ","
}
return ips
}
return ""
}
func organizationalUnits(cert *x509.Certificate) string {
if len(cert.Subject.OrganizationalUnit) > 0 {
return "," + strings.Join(cert.Subject.OrganizationalUnit, ",") + ","
}
return ""
}

223
prober/metrics_test.go Normal file
View File

@@ -0,0 +1,223 @@
package prober
import (
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"fmt"
"reflect"
"strconv"
"strings"
"testing"
"github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go"
"golang.org/x/crypto/ocsp"
)
type registryResult struct {
Name string
LabelValues map[string]string
Value float64
}
func (rr *registryResult) String() string {
var labels []string
for k, v := range rr.LabelValues {
labels = append(labels, k+"=\""+v+"\"")
}
m := rr.Name
if len(labels) > 0 {
m = fmt.Sprintf("%s{%s}", m, strings.Join(labels, ","))
}
return fmt.Sprintf("%s %f", m, rr.Value)
}
func checkRegistryResults(expectedResults []*registryResult, mfs []*dto.MetricFamily, t *testing.T) {
for _, expRes := range expectedResults {
checkRegistryResult(expRes, mfs, t)
}
}
func checkRegistryResult(expRes *registryResult, mfs []*dto.MetricFamily, t *testing.T) {
var results []*registryResult
for _, mf := range mfs {
for _, metric := range mf.Metric {
result := &registryResult{
Name: mf.GetName(),
Value: metric.GetGauge().GetValue(),
}
if len(metric.GetLabel()) > 0 {
labelValues := make(map[string]string)
for _, l := range metric.GetLabel() {
labelValues[l.GetName()] = l.GetValue()
}
result.LabelValues = labelValues
}
results = append(results, result)
}
}
var ok bool
var resStr string
for _, res := range results {
resStr = resStr + "\n" + res.String()
if reflect.DeepEqual(res, expRes) {
ok = true
}
}
if !ok {
t.Fatalf("Expected %s, got: %s", expRes.String(), resStr)
}
}
func checkCertificateMetrics(cert *x509.Certificate, registry *prometheus.Registry, t *testing.T) {
mfs, err := registry.Gather()
if err != nil {
t.Fatal(err)
}
ips := ","
for _, ip := range cert.IPAddresses {
ips = ips + ip.String() + ","
}
expectedLabels := map[string]string{
"serial_no": cert.SerialNumber.String(),
"issuer_cn": cert.Issuer.CommonName,
"cn": cert.Subject.CommonName,
"dnsnames": "," + strings.Join(cert.DNSNames, ",") + ",",
"ips": ips,
"emails": "," + strings.Join(cert.EmailAddresses, ",") + ",",
"ou": "," + strings.Join(cert.Subject.OrganizationalUnit, ",") + ",",
}
expectedResults := []*registryResult{
&registryResult{
Name: "ssl_cert_not_after",
LabelValues: expectedLabels,
Value: float64(cert.NotAfter.Unix()),
},
&registryResult{
Name: "ssl_cert_not_before",
LabelValues: expectedLabels,
Value: float64(cert.NotBefore.Unix()),
},
}
checkRegistryResults(expectedResults, mfs, t)
}
func checkVerifiedChainMetrics(verifiedChains [][]*x509.Certificate, registry *prometheus.Registry, t *testing.T) {
mfs, err := registry.Gather()
if err != nil {
t.Fatal(err)
}
for i, chain := range verifiedChains {
for _, cert := range chain {
ips := ","
for _, ip := range cert.IPAddresses {
ips = ips + ip.String() + ","
}
expectedLabels := map[string]string{
"chain_no": strconv.Itoa(i),
"serial_no": cert.SerialNumber.String(),
"issuer_cn": cert.Issuer.CommonName,
"cn": cert.Subject.CommonName,
"dnsnames": "," + strings.Join(cert.DNSNames, ",") + ",",
"ips": ips,
"emails": "," + strings.Join(cert.EmailAddresses, ",") + ",",
"ou": "," + strings.Join(cert.Subject.OrganizationalUnit, ",") + ",",
}
expectedResults := []*registryResult{
&registryResult{
Name: "ssl_verified_cert_not_after",
LabelValues: expectedLabels,
Value: float64(cert.NotAfter.Unix()),
},
&registryResult{
Name: "ssl_verified_cert_not_before",
LabelValues: expectedLabels,
Value: float64(cert.NotBefore.Unix()),
},
}
checkRegistryResults(expectedResults, mfs, t)
}
}
}
func checkOCSPMetrics(resp []byte, registry *prometheus.Registry, t *testing.T) {
var (
stapled float64
status float64
nextUpdate float64
thisUpdate float64
revokedAt float64
producedAt float64
)
mfs, err := registry.Gather()
if err != nil {
t.Fatal(err)
}
if len(resp) > 0 {
parsedResponse, err := ocsp.ParseResponse(resp, nil)
if err != nil {
t.Fatal(err)
}
stapled = 1
status = float64(parsedResponse.Status)
nextUpdate = float64(parsedResponse.NextUpdate.Unix())
thisUpdate = float64(parsedResponse.ThisUpdate.Unix())
revokedAt = float64(parsedResponse.RevokedAt.Unix())
producedAt = float64(parsedResponse.ProducedAt.Unix())
}
expectedResults := []*registryResult{
&registryResult{
Name: "ssl_ocsp_response_stapled",
Value: stapled,
},
&registryResult{
Name: "ssl_ocsp_response_status",
Value: status,
},
&registryResult{
Name: "ssl_ocsp_response_next_update",
Value: nextUpdate,
},
&registryResult{
Name: "ssl_ocsp_response_this_update",
Value: thisUpdate,
},
&registryResult{
Name: "ssl_ocsp_response_revoked_at",
Value: revokedAt,
},
&registryResult{
Name: "ssl_ocsp_response_produced_at",
Value: producedAt,
},
}
checkRegistryResults(expectedResults, mfs, t)
}
func checkTLSVersionMetrics(version string, registry *prometheus.Registry, t *testing.T) {
mfs, err := registry.Gather()
if err != nil {
t.Fatal(err)
}
expectedResults := []*registryResult{
&registryResult{
Name: "ssl_tls_version_info",
LabelValues: map[string]string{
"version": version,
},
Value: 1,
},
}
checkRegistryResults(expectedResults, mfs, t)
}
func newCertificate(certPEM []byte) (*x509.Certificate, error) {
block, _ := pem.Decode(certPEM)
return x509.ParseCertificate(block.Bytes)
}
func newKey(keyPEM []byte) (*rsa.PrivateKey, error) {
block, _ := pem.Decode([]byte(keyPEM))
return x509.ParsePKCS1PrivateKey(block.Bytes)
}

25
prober/prober.go Normal file
View File

@@ -0,0 +1,25 @@
package prober
import (
"context"
"github.com/go-kit/log"
"github.com/prometheus/client_golang/prometheus"
"github.com/ribbybibby/ssl_exporter/v2/config"
)
var (
// Probers maps a friendly name to a corresponding probe function
Probers = map[string]ProbeFn{
"https": ProbeHTTPS,
"http": ProbeHTTPS,
"tcp": ProbeTCP,
"file": ProbeFile,
"http_file": ProbeHTTPFile,
"kubernetes": ProbeKubernetes,
"kubeconfig": ProbeKubeconfig,
}
)
// ProbeFn probes
type ProbeFn func(ctx context.Context, logger log.Logger, target string, module config.Module, registry *prometheus.Registry) error

194
prober/tcp.go Normal file
View File

@@ -0,0 +1,194 @@
package prober
import (
"bufio"
"bytes"
"context"
"crypto/tls"
"fmt"
"io"
"net"
"regexp"
"github.com/go-kit/log"
"github.com/go-kit/log/level"
"github.com/prometheus/client_golang/prometheus"
"github.com/ribbybibby/ssl_exporter/v2/config"
)
// ProbeTCP performs a tcp probe
func ProbeTCP(ctx context.Context, logger log.Logger, target string, module config.Module, registry *prometheus.Registry) error {
tlsConfig, err := newTLSConfig(target, registry, &module.TLSConfig)
if err != nil {
return err
}
dialer := &net.Dialer{}
conn, err := dialer.DialContext(ctx, "tcp", target)
if err != nil {
return err
}
defer conn.Close()
deadline, _ := ctx.Deadline()
if err := conn.SetDeadline(deadline); err != nil {
return fmt.Errorf("Error setting deadline")
}
if module.TCP.StartTLS != "" {
err = startTLS(logger, conn, module.TCP.StartTLS)
if err != nil {
return err
}
}
tlsConn := tls.Client(conn, tlsConfig)
defer tlsConn.Close()
return tlsConn.Handshake()
}
type queryResponse struct {
expect string
send string
sendBytes []byte
expectBytes []byte
}
var (
// These are the protocols for which I had servers readily available to test
// against. There are plenty of other protocols that should be added here in
// the future.
//
// See openssl s_client for more examples:
// https://github.com/openssl/openssl/blob/openssl-3.0.0-alpha3/apps/s_client.c#L2229-L2728
startTLSqueryResponses = map[string][]queryResponse{
"smtp": []queryResponse{
queryResponse{
expect: "^220",
},
queryResponse{
send: "EHLO prober",
},
queryResponse{
expect: "^250(-| )STARTTLS",
},
queryResponse{
send: "STARTTLS",
},
queryResponse{
expect: "^220",
},
},
"ftp": []queryResponse{
queryResponse{
expect: "^220",
},
queryResponse{
send: "AUTH TLS",
},
queryResponse{
expect: "^234",
},
},
"imap": []queryResponse{
queryResponse{
expect: "OK",
},
queryResponse{
send: ". CAPABILITY",
},
queryResponse{
expect: "STARTTLS",
},
queryResponse{
expect: "OK",
},
queryResponse{
send: ". STARTTLS",
},
queryResponse{
expect: "OK",
},
},
"postgres": []queryResponse{
queryResponse{
sendBytes: []byte{0x00, 0x00, 0x00, 0x08, 0x04, 0xd2, 0x16, 0x2f},
},
queryResponse{
expectBytes: []byte{0x53},
},
},
"pop3": []queryResponse{
queryResponse{
expect: "OK",
},
queryResponse{
send: "STLS",
},
queryResponse{
expect: "OK",
},
},
}
)
// startTLS will send the STARTTLS command for the given protocol
func startTLS(logger log.Logger, conn net.Conn, proto string) error {
var err error
qr, ok := startTLSqueryResponses[proto]
if !ok {
return fmt.Errorf("STARTTLS is not supported for %s", proto)
}
scanner := bufio.NewScanner(conn)
for _, qr := range qr {
if qr.expect != "" {
var match bool
for scanner.Scan() {
level.Debug(logger).Log("msg", fmt.Sprintf("read line: %s", scanner.Text()))
match, err = regexp.Match(qr.expect, scanner.Bytes())
if err != nil {
return err
}
if match {
level.Debug(logger).Log("msg", fmt.Sprintf("regex: %s matched: %s", qr.expect, scanner.Text()))
break
}
}
if scanner.Err() != nil {
return scanner.Err()
}
if !match {
return fmt.Errorf("regex: %s didn't match: %s", qr.expect, scanner.Text())
}
}
if len(qr.expectBytes) > 0 {
buffer := make([]byte, len(qr.expectBytes))
_, err = io.ReadFull(conn, buffer)
if err != nil {
return nil
}
level.Debug(logger).Log("msg", fmt.Sprintf("read bytes: %x", buffer))
if bytes.Compare(buffer, qr.expectBytes) != 0 {
return fmt.Errorf("read bytes %x didn't match with expected bytes %x", buffer, qr.expectBytes)
} else {
level.Debug(logger).Log("msg", fmt.Sprintf("expected bytes %x matched with read bytes %x", qr.expectBytes, buffer))
}
}
if qr.send != "" {
level.Debug(logger).Log("msg", fmt.Sprintf("sending line: %s", qr.send))
if _, err := fmt.Fprintf(conn, "%s\r\n", qr.send); err != nil {
return err
}
}
if len(qr.sendBytes) > 0 {
level.Debug(logger).Log("msg", fmt.Sprintf("sending bytes: %x", qr.sendBytes))
if _, err = conn.Write(qr.sendBytes); err != nil {
return err
}
}
}
return nil
}

601
prober/tcp_test.go Normal file
View File

@@ -0,0 +1,601 @@
package prober
import (
"bytes"
"context"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"math/big"
"net"
"testing"
"time"
"github.com/ribbybibby/ssl_exporter/v2/config"
"github.com/ribbybibby/ssl_exporter/v2/test"
"golang.org/x/crypto/ocsp"
"github.com/prometheus/client_golang/prometheus"
)
// TestProbeTCP tests the typical case
func TestProbeTCP(t *testing.T) {
server, certPEM, _, caFile, teardown, err := test.SetupTCPServer()
if err != nil {
t.Fatal(err)
}
defer teardown()
server.StartTLS()
defer server.Close()
module := config.Module{
TLSConfig: config.TLSConfig{
CAFile: caFile,
InsecureSkipVerify: false,
},
}
registry := prometheus.NewRegistry()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := ProbeTCP(ctx, newTestLogger(), server.Listener.Addr().String(), module, registry); err != nil {
t.Fatalf("error: %s", err)
}
cert, err := newCertificate(certPEM)
if err != nil {
t.Fatal(err)
}
checkCertificateMetrics(cert, registry, t)
checkOCSPMetrics([]byte{}, registry, t)
checkTLSVersionMetrics("TLS 1.3", registry, t)
}
// TestProbeTCPInvalidName tests hitting the server on an address which isn't
// in the SANs (localhost)
func TestProbeTCPInvalidName(t *testing.T) {
server, _, _, caFile, teardown, err := test.SetupTCPServer()
if err != nil {
t.Fatalf(err.Error())
}
defer teardown()
server.StartTLS()
defer server.Close()
module := config.Module{
TLSConfig: config.TLSConfig{
CAFile: caFile,
InsecureSkipVerify: false,
},
}
_, listenPort, _ := net.SplitHostPort(server.Listener.Addr().String())
registry := prometheus.NewRegistry()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := ProbeTCP(ctx, newTestLogger(), "localhost:"+listenPort, module, registry); err == nil {
t.Fatalf("expected error but err was nil")
}
}
// TestProbeTCPServerName tests that the probe is successful when the
// servername is provided in the TLS config
func TestProbeTCPServerName(t *testing.T) {
server, certPEM, _, caFile, teardown, err := test.SetupTCPServer()
if err != nil {
t.Fatalf(err.Error())
}
defer teardown()
server.StartTLS()
defer server.Close()
host, listenPort, _ := net.SplitHostPort(server.Listener.Addr().String())
module := config.Module{
TLSConfig: config.TLSConfig{
CAFile: caFile,
InsecureSkipVerify: false,
ServerName: host,
},
}
registry := prometheus.NewRegistry()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := ProbeTCP(ctx, newTestLogger(), "localhost:"+listenPort, module, registry); err != nil {
t.Fatalf("error: %s", err)
}
cert, err := newCertificate(certPEM)
if err != nil {
t.Fatal(err)
}
checkCertificateMetrics(cert, registry, t)
checkOCSPMetrics([]byte{}, registry, t)
checkTLSVersionMetrics("TLS 1.3", registry, t)
}
// TestProbeTCPExpired tests that the probe fails with an expired server cert
func TestProbeTCPExpired(t *testing.T) {
server, _, _, caFile, teardown, err := test.SetupTCPServer()
if err != nil {
t.Fatalf(err.Error())
}
defer teardown()
// Create a certificate with a notAfter date in the past
certPEM, keyPEM := test.GenerateTestCertificate(time.Now().AddDate(0, 0, -1))
testcert, err := tls.X509KeyPair(certPEM, keyPEM)
if err != nil {
t.Fatalf(err.Error())
}
server.TLS.Certificates = []tls.Certificate{testcert}
server.StartTLS()
defer server.Close()
module := config.Module{
TLSConfig: config.TLSConfig{
CAFile: caFile,
InsecureSkipVerify: false,
},
}
registry := prometheus.NewRegistry()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := ProbeTCP(ctx, newTestLogger(), server.Listener.Addr().String(), module, registry); err == nil {
t.Fatalf("expected error but err is nil")
}
}
// TestProbeTCPExpiredInsecure tests that the probe succeeds with an expired server cert
// when skipping cert verification
func TestProbeTCPExpiredInsecure(t *testing.T) {
server, certPEM, _, caFile, teardown, err := test.SetupTCPServer()
if err != nil {
t.Fatalf(err.Error())
}
defer teardown()
// Create a certificate with a notAfter date in the past
certPEM, keyPEM := test.GenerateTestCertificate(time.Now().AddDate(0, 0, -1))
testcert, err := tls.X509KeyPair(certPEM, keyPEM)
if err != nil {
t.Fatalf(err.Error())
}
server.TLS.Certificates = []tls.Certificate{testcert}
server.StartTLS()
defer server.Close()
module := config.Module{
TLSConfig: config.TLSConfig{
CAFile: caFile,
InsecureSkipVerify: true,
},
}
registry := prometheus.NewRegistry()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := ProbeTCP(ctx, newTestLogger(), server.Listener.Addr().String(), module, registry); err != nil {
t.Fatalf("error: %s", err)
}
cert, err := newCertificate(certPEM)
if err != nil {
t.Fatal(err)
}
checkCertificateMetrics(cert, registry, t)
checkOCSPMetrics([]byte{}, registry, t)
checkTLSVersionMetrics("TLS 1.3", registry, t)
}
// TestProbeTCPStartTLSSMTP tests STARTTLS against a mock SMTP server
func TestProbeTCPStartTLSSMTP(t *testing.T) {
server, certPEM, _, caFile, teardown, err := test.SetupTCPServer()
if err != nil {
t.Fatalf(err.Error())
}
defer teardown()
server.StartSMTP()
defer server.Close()
module := config.Module{
TCP: config.TCPProbe{
StartTLS: "smtp",
},
TLSConfig: config.TLSConfig{
CAFile: caFile,
InsecureSkipVerify: false,
},
}
registry := prometheus.NewRegistry()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := ProbeTCP(ctx, newTestLogger(), server.Listener.Addr().String(), module, registry); err != nil {
t.Fatalf("error: %s", err)
}
cert, err := newCertificate(certPEM)
if err != nil {
t.Fatal(err)
}
checkCertificateMetrics(cert, registry, t)
checkOCSPMetrics([]byte{}, registry, t)
checkTLSVersionMetrics("TLS 1.3", registry, t)
}
// TestProbeTCPStartTLSSMTPWithDashInResponse tests STARTTLS against a mock SMTP server
// which provides STARTTLS as option with dash which is okay when it used as the last option
func TestProbeTCPStartTLSSMTPWithDashInResponse(t *testing.T) {
server, certPEM, _, caFile, teardown, err := test.SetupTCPServer()
if err != nil {
t.Fatalf(err.Error())
}
defer teardown()
server.StartSMTPWithDashInResponse()
defer server.Close()
module := config.Module{
TCP: config.TCPProbe{
StartTLS: "smtp",
},
TLSConfig: config.TLSConfig{
CAFile: caFile,
InsecureSkipVerify: false,
},
}
registry := prometheus.NewRegistry()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := ProbeTCP(ctx, newTestLogger(), server.Listener.Addr().String(), module, registry); err != nil {
t.Fatalf("error: %s", err)
}
cert, err := newCertificate(certPEM)
if err != nil {
t.Fatal(err)
}
checkCertificateMetrics(cert, registry, t)
checkOCSPMetrics([]byte{}, registry, t)
checkTLSVersionMetrics("TLS 1.3", registry, t)
}
// TestProbeTCPStartTLSFTP tests STARTTLS against a mock FTP server
func TestProbeTCPStartTLSFTP(t *testing.T) {
server, certPEM, _, caFile, teardown, err := test.SetupTCPServer()
if err != nil {
t.Fatalf(err.Error())
}
defer teardown()
server.StartFTP()
defer server.Close()
module := config.Module{
TCP: config.TCPProbe{
StartTLS: "ftp",
},
TLSConfig: config.TLSConfig{
CAFile: caFile,
InsecureSkipVerify: false,
},
}
registry := prometheus.NewRegistry()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := ProbeTCP(ctx, newTestLogger(), server.Listener.Addr().String(), module, registry); err != nil {
t.Fatalf("error: %s", err)
}
cert, err := newCertificate(certPEM)
if err != nil {
t.Fatal(err)
}
checkCertificateMetrics(cert, registry, t)
checkOCSPMetrics([]byte{}, registry, t)
checkTLSVersionMetrics("TLS 1.3", registry, t)
}
// TestProbeTCPStartTLSIMAP tests STARTTLS against a mock IMAP server
func TestProbeTCPStartTLSIMAP(t *testing.T) {
server, certPEM, _, caFile, teardown, err := test.SetupTCPServer()
if err != nil {
t.Fatalf(err.Error())
}
defer teardown()
server.StartIMAP()
defer server.Close()
module := config.Module{
TCP: config.TCPProbe{
StartTLS: "imap",
},
TLSConfig: config.TLSConfig{
CAFile: caFile,
InsecureSkipVerify: false,
},
}
registry := prometheus.NewRegistry()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := ProbeTCP(ctx, newTestLogger(), server.Listener.Addr().String(), module, registry); err != nil {
t.Fatalf("error: %s", err)
}
cert, err := newCertificate(certPEM)
if err != nil {
t.Fatal(err)
}
checkCertificateMetrics(cert, registry, t)
checkOCSPMetrics([]byte{}, registry, t)
checkTLSVersionMetrics("TLS 1.3", registry, t)
}
// TestProbeTCPStartTLSPOP3 tests STARTTLS against a mock POP3 server
func TestProbeTCPStartTLSPOP3(t *testing.T) {
server, certPEM, _, caFile, teardown, err := test.SetupTCPServer()
if err != nil {
t.Fatalf(err.Error())
}
defer teardown()
server.StartPOP3()
defer server.Close()
module := config.Module{
TCP: config.TCPProbe{
StartTLS: "pop3",
},
TLSConfig: config.TLSConfig{
CAFile: caFile,
InsecureSkipVerify: false,
},
}
registry := prometheus.NewRegistry()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := ProbeTCP(ctx, newTestLogger(), server.Listener.Addr().String(), module, registry); err != nil {
t.Fatalf("error: %s", err)
}
cert, err := newCertificate(certPEM)
if err != nil {
t.Fatal(err)
}
checkCertificateMetrics(cert, registry, t)
checkOCSPMetrics([]byte{}, registry, t)
checkTLSVersionMetrics("TLS 1.3", registry, t)
}
// TestProbeTCPStartTLSPostgreSQL tests STARTTLS against a mock PostgreSQL server
func TestProbeTCPStartTLSPostgreSQL(t *testing.T) {
server, certPEM, _, caFile, teardown, err := test.SetupTCPServer()
if err != nil {
t.Fatalf(err.Error())
}
defer teardown()
server.StartPostgreSQL()
defer server.Close()
module := config.Module{
TCP: config.TCPProbe{
StartTLS: "postgres",
},
TLSConfig: config.TLSConfig{
CAFile: caFile,
InsecureSkipVerify: false,
},
}
registry := prometheus.NewRegistry()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := ProbeTCP(ctx, newTestLogger(), server.Listener.Addr().String(), module, registry); err != nil {
t.Fatalf("error: %s", err)
}
cert, err := newCertificate(certPEM)
if err != nil {
t.Fatal(err)
}
checkCertificateMetrics(cert, registry, t)
checkOCSPMetrics([]byte{}, registry, t)
checkTLSVersionMetrics("TLS 1.3", registry, t)
}
// TestProbeTCPTimeout tests that the TCP probe respects the timeout in the
// context
func TestProbeTCPTimeout(t *testing.T) {
server, _, _, caFile, teardown, err := test.SetupTCPServer()
if err != nil {
t.Fatal(err)
}
defer teardown()
server.StartTLSWait(time.Second * 3)
defer server.Close()
module := config.Module{
TLSConfig: config.TLSConfig{
CAFile: caFile,
InsecureSkipVerify: false,
},
}
registry := prometheus.NewRegistry()
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
if err := ProbeTCP(ctx, newTestLogger(), server.Listener.Addr().String(), module, registry); err == nil {
t.Fatalf("Expected error but returned error was nil")
}
}
// TestProbeTCPOCSP tests a TCP probe with OCSP stapling
func TestProbeTCPOCSP(t *testing.T) {
server, certPEM, keyPEM, caFile, teardown, err := test.SetupTCPServer()
if err != nil {
t.Fatal(err)
}
defer teardown()
cert, err := newCertificate(certPEM)
if err != nil {
t.Fatal(err)
}
key, err := newKey(keyPEM)
if err != nil {
t.Fatal(err)
}
resp, err := ocsp.CreateResponse(cert, cert, ocsp.Response{SerialNumber: big.NewInt(64), Status: 1}, key)
if err != nil {
t.Fatalf(err.Error())
}
server.TLS.Certificates[0].OCSPStaple = resp
server.StartTLS()
defer server.Close()
module := config.Module{
TLSConfig: config.TLSConfig{
CAFile: caFile,
InsecureSkipVerify: false,
},
}
registry := prometheus.NewRegistry()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := ProbeTCP(ctx, newTestLogger(), server.Listener.Addr().String(), module, registry); err != nil {
t.Fatalf("error: %s", err)
}
checkCertificateMetrics(cert, registry, t)
checkOCSPMetrics(resp, registry, t)
checkTLSVersionMetrics("TLS 1.3", registry, t)
}
// TestProbeTCPVerifiedChains tests the verified chain metrics returned by a tcp
// probe
func TestProbeTCPVerifiedChains(t *testing.T) {
rootPrivateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatalf(err.Error())
}
rootCertExpiry := time.Now().AddDate(0, 0, 5)
rootCertTmpl := test.GenerateCertificateTemplate(rootCertExpiry)
rootCertTmpl.IsCA = true
rootCertTmpl.SerialNumber = big.NewInt(1)
rootCert, rootCertPem := test.GenerateSelfSignedCertificateWithPrivateKey(rootCertTmpl, rootPrivateKey)
olderRootCertExpiry := time.Now().AddDate(0, 0, 3)
olderRootCertTmpl := test.GenerateCertificateTemplate(olderRootCertExpiry)
olderRootCertTmpl.IsCA = true
olderRootCertTmpl.SerialNumber = big.NewInt(2)
olderRootCert, olderRootCertPem := test.GenerateSelfSignedCertificateWithPrivateKey(olderRootCertTmpl, rootPrivateKey)
oldestRootCertExpiry := time.Now().AddDate(0, 0, 1)
oldestRootCertTmpl := test.GenerateCertificateTemplate(oldestRootCertExpiry)
oldestRootCertTmpl.IsCA = true
oldestRootCertTmpl.SerialNumber = big.NewInt(3)
oldestRootCert, oldestRootCertPem := test.GenerateSelfSignedCertificateWithPrivateKey(oldestRootCertTmpl, rootPrivateKey)
serverCertExpiry := time.Now().AddDate(0, 0, 4)
serverCertTmpl := test.GenerateCertificateTemplate(serverCertExpiry)
serverCertTmpl.SerialNumber = big.NewInt(4)
serverCert, serverCertPem, serverKey := test.GenerateSignedCertificate(serverCertTmpl, olderRootCert, rootPrivateKey)
verifiedChains := [][]*x509.Certificate{
[]*x509.Certificate{
serverCert,
rootCert,
},
[]*x509.Certificate{
serverCert,
olderRootCert,
},
[]*x509.Certificate{
serverCert,
oldestRootCert,
},
}
caCertPem := bytes.Join([][]byte{oldestRootCertPem, olderRootCertPem, rootCertPem}, []byte(""))
server, caFile, teardown, err := test.SetupTCPServerWithCertAndKey(
caCertPem,
serverCertPem,
serverKey,
)
if err != nil {
t.Fatalf(err.Error())
}
defer teardown()
server.StartTLS()
defer server.Close()
module := config.Module{
TLSConfig: config.TLSConfig{
CAFile: caFile,
},
}
registry := prometheus.NewRegistry()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := ProbeTCP(ctx, newTestLogger(), server.Listener.Addr().String(), module, registry); err != nil {
t.Fatalf("error: %s", err)
}
checkCertificateMetrics(serverCert, registry, t)
checkOCSPMetrics([]byte{}, registry, t)
checkVerifiedChainMetrics(verifiedChains, registry, t)
checkTLSVersionMetrics("TLS 1.3", registry, t)
}

11
prober/test.go Normal file
View File

@@ -0,0 +1,11 @@
package prober
import (
"os"
"github.com/go-kit/log"
)
func newTestLogger() log.Logger {
return log.NewLogfmtLogger(log.NewSyncWriter(os.Stdout))
}

72
prober/tls.go Normal file
View File

@@ -0,0 +1,72 @@
package prober
import (
"crypto/tls"
"crypto/x509"
"encoding/pem"
"net"
"github.com/prometheus/client_golang/prometheus"
"github.com/ribbybibby/ssl_exporter/v2/config"
)
// newTLSConfig sets up TLS config and instruments it with a function that
// collects metrics for the verified chain
func newTLSConfig(target string, registry *prometheus.Registry, cfg *config.TLSConfig) (*tls.Config, error) {
tlsConfig, err := config.NewTLSConfig(cfg)
if err != nil {
return nil, err
}
if tlsConfig.ServerName == "" && target != "" {
targetAddress, _, err := net.SplitHostPort(target)
if err != nil {
return nil, err
}
tlsConfig.ServerName = targetAddress
}
tlsConfig.VerifyConnection = func(state tls.ConnectionState) error {
return collectConnectionStateMetrics(state, registry)
}
return tlsConfig, nil
}
func uniq(certs []*x509.Certificate) []*x509.Certificate {
r := []*x509.Certificate{}
for _, c := range certs {
if !contains(r, c) {
r = append(r, c)
}
}
return r
}
func contains(certs []*x509.Certificate, cert *x509.Certificate) bool {
for _, c := range certs {
if (c.SerialNumber.String() == cert.SerialNumber.String()) && (c.Issuer.CommonName == cert.Issuer.CommonName) {
return true
}
}
return false
}
func decodeCertificates(data []byte) ([]*x509.Certificate, error) {
var certs []*x509.Certificate
for block, rest := pem.Decode(data); block != nil; block, rest = pem.Decode(rest) {
if block.Type == "CERTIFICATE" || block.Type == "TRUSTED CERTIFICATE" {
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return certs, err
}
if !contains(certs, cert) {
certs = append(certs, cert)
}
}
}
return certs, nil
}

86
prober/tls_test.go Normal file
View File

@@ -0,0 +1,86 @@
package prober
import (
"testing"
)
func TestDecodeCertificates(t *testing.T) {
data := []byte(`
-----BEGIN CERTIFICATE-----
MIIFszCCA5ugAwIBAgIUdpzowWDU/AI7QBhLSRB9DPqpWvcwDQYJKoZIhvcNAQEL
BQAwaTELMAkGA1UEBhMCWFgxEjAQBgNVBAgMCVN0YXRlTmFtZTERMA8GA1UEBwwI
Q2l0eU5hbWUxFDASBgNVBAoMC0NvbXBhbnlOYW1lMQwwCgYDVQQLDANGb28xDzAN
BgNVBAMMBkZvb2JhcjAeFw0yNDA0MjgxNjIzMzNaFw0zNDA0MjYxNjIzMzNaMGkx
CzAJBgNVBAYTAlhYMRIwEAYDVQQIDAlTdGF0ZU5hbWUxETAPBgNVBAcMCENpdHlO
YW1lMRQwEgYDVQQKDAtDb21wYW55TmFtZTEMMAoGA1UECwwDRm9vMQ8wDQYDVQQD
DAZGb29iYXIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCmxZBW/Ays
VBxt7jJQeTrPdLQpUdxnGVXOa4M54FHWA2DwwmZ2DZth5Eioq1wC9WCrByWkd8px
mvU0XDUT5ESceEKcwmDhKgYHAcJ4qXEyk1jYuBy6zw95cmV2BiTf0Xoo/8JxiQR4
YBd7Tbm52eV5Hw5oaqgVEdaOCVMnO8S57AuQGfeC5AO18ty0cZ7mKsXQje2celMH
QipujXrhRwdLBgu6FISTuS0XtqiuJnp+vllMjiTMF/uCmJUTUCpmayWSpM1FRpKe
lM8B9666uuEmVJ4V5gzy4Oe0i5Apfh4qXX6pj+Y/oeOfXRc3NfYqcIJ2hm82ghJL
5Kt6FZ9fkQ6pyk7nXMAOaf8WX+JkhqaWvlzTme8Z+6DBXLPfyyoi3VR2kG3lRida
qfyh2EPvEXip810s4f8EOHi+sehmjWLbsgn2HAHQmE7zYTEMp2Xsh0M7lGfUiMfP
P1RU/RNDkZK59yXsG9RmoDMD03qI8M4990TL7BW0FQZvBg9Rfr3KgFpWngsn84mN
l6/MKyc5X1e+RGyYJPbi0S1j+fhBeNzFqQPFZZUnWIPTxVdqF91SgY9EwE6MHuD6
t7G+eANWWQkolvwACMT+0GqiRMVHVWeIqF2SQ7Dx2tiNXjE9rSX3lMpMNt4PvWcM
PGuao3FMqXts0j2L8FSljcVU/hiLOGC2mwIDAQABo1MwUTAdBgNVHQ4EFgQU+231
i0rY0CwQ0JwT2EUX4cq81LIwHwYDVR0jBBgwFoAU+231i0rY0CwQ0JwT2EUX4cq8
1LIwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAgEAVRqUVxovcZ9C
BYUUhvyT8BMAC6PS8U59BoDhnbrp1JDiPDbMnWlRp+450kZlEkSdN9PRLGWoGi84
QqT7RIry/jA4z62B6N+IDDDcuWWstDTTC/gRA46eeHGRnIz7GJeseqrxb9z8KoO3
/c8Pdb4lu/cfG5/m2X8hkYzjLZrIQ8qz53nUAag4uael19uy3HPQ3EEr936y+vQW
rH8itgQ+5kOTr9d3ihcTSwTJGTyWq7j3T0xPu9tCzoPragpEDjoUCqCjU7uv6Tdq
UhaQ8vneimSf9VdbDfEuEU9S2Xbdg6e+BsdV9uMeWkhtD1WgtSLHcliYktwnh/kh
r4vQG86xn+LDxTrdTssjt+UmJnXzRiGOEZCDz7kBchYLiWSQn9tqcn28KK2/YobD
AghlR24hUL1uslJOjLFc4BwLmlv/4Iy+b/3iQWY+yoban/OLZo6gCZx/4Nvg5R9e
tcuxgn5Jhm2T/e8REucXQfiqKgxQVibFmqXeLH3Yj7ussA5t/ozo4VuypXUnnil0
hB+PQsViaHFId1LyBKFwMoA6JzlNle6cbWVtXJswffkjk0UUr26SLHMV87lrk2kn
IQ7ROZcT8K7gzzTxuiC2g6npmG1fDfgmgJeS7IqgiFcRTKa0hj6bvc/WoHoCpLNs
v5KXxrGlqibeNjyc3Wng6S0Kpg6YNqU=
-----END CERTIFICATE-----
-----BEGIN TRUSTED CERTIFICATE-----
MIIF7zCCA9egAwIBAgIUZXsnB0gxoPSFaUjnQeLwTW+sqbYwDQYJKoZIhvcNAQEL
BQAwgYYxCzAJBgNVBAYTAlhYMRIwEAYDVQQIDAlTdGF0ZU5hbWUxETAPBgNVBAcM
CENpdHlOYW1lMRQwEgYDVQQKDAtDb21wYW55TmFtZTEbMBkGA1UECwwSQ29tcGFu
eVNlY3Rpb25OYW1lMR0wGwYDVQQDDBRDb21tb25OYW1lT3JIb3N0bmFtZTAeFw0y
NDA0MjgxNjE3MzZaFw0zNDA0MjYxNjE3MzZaMIGGMQswCQYDVQQGEwJYWDESMBAG
A1UECAwJU3RhdGVOYW1lMREwDwYDVQQHDAhDaXR5TmFtZTEUMBIGA1UECgwLQ29t
cGFueU5hbWUxGzAZBgNVBAsMEkNvbXBhbnlTZWN0aW9uTmFtZTEdMBsGA1UEAwwU
Q29tbW9uTmFtZU9ySG9zdG5hbWUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK
AoICAQDJ+g1z+nZFj0DFc+wV1AH4giAqDFhwx25yziaQJnHkoXpllN0bRpr50Fhx
QsMuMqjMCM0eFPo7kDWSrAUusdh3j1jdF7KidZRZdyUgioQvbmNtqtwdU/38Gq0s
yTspOTZTWziIMFsLYxvFb0Ia8dwQFvyDB3pM1/eXFJMr2Cz/nF4/g4IUFEtPY2pe
4sQDMhszvROMfI5LB4SmGiim/zUS7+ZDEQigf10/CrbLntjXHiFy+V0hcrDQXkuY
gXU9HsiwRpCx+GRqcI6SWPIZ2rYa7goAVBENv5KWFXdWaOFCpi55XldYYzDeaXWa
On9EOjyylLbJ2pv/yhpt/VYDC6Lsqki1XcU/zeETcBRlqzCwZhMqFErSPRBGxaTN
lEGKNS2qQnE7I7bmUksRcCxA0WIn4V2xxxGHkLFnEsCipFGgMm7LaGSukYZGQ2MF
0ELqaaRMaTBfvPhFRtolkFChm7JQlCrk9j47b07OTMj4KUoBkhYPNoK26sHjvKue
Iks7BTGy8NdZk3jVDNEgGwngj+bNyJcjDvq+cHhWhS/N8c8Tg2tFb3ZOnwbyFPLf
1DdsP/SW6TybMc6NtpWvmSCFMvxqRpr+7LscpBGThepAUaoPkATnT6UdYGqr+l5N
z1+cZEuHBF7Jk5xThXCCFwuBC/KREvRuU2LyAhm9RV1ci8XyYwIDAQABo1MwUTAd
BgNVHQ4EFgQUa1FF7jssOpGY0UY1qbFQH83rxOQwHwYDVR0jBBgwFoAUa1FF7jss
OpGY0UY1qbFQH83rxOQwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC
AgEAhRPDPmVetjE5w/igfnGiNUKhQiqB3E6yT1wMwF5LbNIC+h8E8WqF0mzF3XQC
t9+Eib+mFptHGfRGdA8JmJ79xBgch6JLagw8Ot6hA1oEqWy/PAzcFLemccvhonhZ
+N7umRO27agIYVJ71mxlS7rD1SItBKZ+g4/Lt3sr/iDM0kIhWjWdQiYgJMNhH2Mt
XAd53tmPSom1Vsca1ZorB0HJNIw8RB7QYwceAi0xk0Tui7Z6pHg8p99XHCK/2vD+
+u5a1nHjtrAvLdNti8o82EUScK4j91CODCm5bv2KDMIUUv+1O83UMicK8DLSIi0P
1acuIdbRrY5JiYewCC+NFCfODZT6y00QL7aF9cbT+ZySiYtpBUiLFlIZCw4dcJ6r
yYJca1gv+kqZugdgLD7nXWIzXgfreQ+Q/BiVouXroXaWqU/9dqzGw4LlPSpyqI9o
nFdu970j4BvVc809i1aNo3odGz9yRx7wOOMyiyHoRpYenlf3hFbqJHS2FUgJ/Ajl
TC1joWDPdwdqQY56WOpk2LMK1KJuMYQEEHb3Ib0ZaxXAT6Nd12VashG5gYhw9vGn
aZ3nCnZFahgB2tB5DtkK+p0PQVpZOv6/0CKSsfQkBFLTjeVIRnlL/UlK3VlwHH9h
0LfsLFabedOS9R1XIaQ7aKXPK4tKmNb5H/ONzH2zxU5Oqoo=
-----END TRUSTED CERTIFICATE-----
`)
certs, err := decodeCertificates(data)
if err != nil {
t.Errorf("unexpected error: %s", err)
}
if len(certs) != 2 {
t.Errorf("unexpected number of certs: %d", len(certs))
}
}

View File

@@ -1,227 +1,122 @@
package main
import (
"crypto/tls"
"crypto/x509"
"context"
"fmt"
"net/http"
"os"
"strconv"
"strings"
"time"
"github.com/alecthomas/kingpin/v2"
"github.com/go-kit/log"
"github.com/go-kit/log/level"
"github.com/prometheus/client_golang/prometheus"
versioncollector "github.com/prometheus/client_golang/prometheus/collectors/version"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/prometheus/common/log"
"github.com/prometheus/common/promlog"
promlogflag "github.com/prometheus/common/promlog/flag"
"github.com/prometheus/common/version"
"gopkg.in/alecthomas/kingpin.v2"
"github.com/ribbybibby/ssl_exporter/v2/config"
"github.com/ribbybibby/ssl_exporter/v2/prober"
)
const (
namespace = "ssl"
)
var (
httpsConnectSuccess = prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "https_connect_success"),
"If the TLS connection was a success",
nil, nil,
)
notBefore = prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "cert_not_before"),
"NotBefore expressed as a Unix Epoch Time",
[]string{"serial_no", "issuer_cn"}, nil,
)
notAfter = prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "cert_not_after"),
"NotAfter expressed as a Unix Epoch Time",
[]string{"serial_no", "issuer_cn"}, nil,
)
commonName = prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "cert_subject_common_name"),
"Subject Common Name",
[]string{"serial_no", "issuer_cn", "subject_cn"}, nil,
)
subjectAlernativeDNSNames = prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "cert_subject_alternative_dnsnames"),
"Subject Alternative DNS Names",
[]string{"serial_no", "issuer_cn", "dnsnames"}, nil,
)
subjectAlernativeIPs = prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "cert_subject_alternative_ips"),
"Subject Alternative DNS Names",
[]string{"serial_no", "issuer_cn", "ips"}, nil,
)
subjectAlernativeEmailAddresses = prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "cert_subject_alternative_emails"),
"Subject Alternative DNS Names",
[]string{"serial_no", "issuer_cn", "emails"}, nil,
)
)
type Exporter struct {
target string
timeout time.Duration
insecure bool
}
func (e *Exporter) Describe(ch chan<- *prometheus.Desc) {
ch <- httpsConnectSuccess
ch <- notAfter
ch <- commonName
ch <- subjectAlernativeDNSNames
ch <- subjectAlernativeIPs
ch <- subjectAlernativeEmailAddresses
}
func (e *Exporter) Collect(ch chan<- prometheus.Metric) {
// Create the HTTP client and make a get request of the target
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: e.insecure},
}
client := &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
Transport: tr,
Timeout: e.timeout,
}
resp, err := client.Get(e.target)
if err != nil {
log.Errorln(err)
ch <- prometheus.MustNewConstMetric(
httpsConnectSuccess, prometheus.GaugeValue, 0,
)
return
}
if resp.TLS == nil {
log.Errorln("The response from " + e.target + " is unencrypted")
ch <- prometheus.MustNewConstMetric(
httpsConnectSuccess, prometheus.GaugeValue, 0,
)
return
}
ch <- prometheus.MustNewConstMetric(
httpsConnectSuccess, prometheus.GaugeValue, 1,
)
peer_certificates := uniq(resp.TLS.PeerCertificates)
// Loop through returned certificates and create metrics
for _, cert := range peer_certificates {
subject_cn := cert.Subject.CommonName
issuer_cn := cert.Issuer.CommonName
subject_dnsn := cert.DNSNames
subject_emails := cert.EmailAddresses
subject_ips := cert.IPAddresses
serial_no := cert.SerialNumber.String()
if !cert.NotAfter.IsZero() {
ch <- prometheus.MustNewConstMetric(
notAfter, prometheus.GaugeValue, float64(cert.NotAfter.UnixNano()/1e9), serial_no, issuer_cn,
)
}
if !cert.NotBefore.IsZero() {
ch <- prometheus.MustNewConstMetric(
notBefore, prometheus.GaugeValue, float64(cert.NotBefore.UnixNano()/1e9), serial_no, issuer_cn,
)
}
if subject_cn != "" {
ch <- prometheus.MustNewConstMetric(
commonName, prometheus.GaugeValue, 1, serial_no, issuer_cn, subject_cn,
)
}
if len(subject_dnsn) > 0 {
ch <- prometheus.MustNewConstMetric(
subjectAlernativeDNSNames, prometheus.GaugeValue, 1, serial_no, issuer_cn, ","+strings.Join(subject_dnsn, ",")+",",
)
}
if len(subject_emails) > 0 {
ch <- prometheus.MustNewConstMetric(
subjectAlernativeEmailAddresses, prometheus.GaugeValue, 1, serial_no, issuer_cn, ","+strings.Join(subject_emails, ",")+",",
)
}
if len(subject_ips) > 0 {
i := ","
for _, ip := range subject_ips {
i = i + ip.String() + ","
}
ch <- prometheus.MustNewConstMetric(
subjectAlernativeIPs, prometheus.GaugeValue, 1, serial_no, issuer_cn, i,
)
}
}
}
func probeHandler(w http.ResponseWriter, r *http.Request, insecure bool) {
target := r.URL.Query().Get("target")
// The following timeout block was taken wholly from the blackbox exporter
// https://github.com/prometheus/blackbox_exporter/blob/master/main.go
var timeoutSeconds float64
if v := r.Header.Get("X-Prometheus-Scrape-Timeout-Seconds"); v != "" {
var err error
timeoutSeconds, err = strconv.ParseFloat(v, 64)
if err != nil {
http.Error(w, fmt.Sprintf("Failed to parse timeout from Prometheus header: %s", err), http.StatusInternalServerError)
func probeHandler(logger log.Logger, w http.ResponseWriter, r *http.Request, conf *config.Config) {
moduleName := r.URL.Query().Get("module")
if moduleName == "" {
moduleName = conf.DefaultModule
if moduleName == "" {
http.Error(w, "Module parameter must be set", http.StatusBadRequest)
return
}
} else {
timeoutSeconds = 10
}
if timeoutSeconds == 0 {
timeoutSeconds = 10
module, ok := conf.Modules[moduleName]
if !ok {
http.Error(w, fmt.Sprintf("Unknown module %q", moduleName), http.StatusBadRequest)
return
}
timeout := time.Duration((timeoutSeconds) * 1e9)
timeout := module.Timeout
if timeout == 0 {
// The following timeout block was taken wholly from the blackbox exporter
// https://github.com/prometheus/blackbox_exporter/blob/master/main.go
var timeoutSeconds float64
if v := r.Header.Get("X-Prometheus-Scrape-Timeout-Seconds"); v != "" {
var err error
timeoutSeconds, err = strconv.ParseFloat(v, 64)
if err != nil {
http.Error(w, fmt.Sprintf("Failed to parse timeout from Prometheus header: %s", err), http.StatusInternalServerError)
return
}
} else {
timeoutSeconds = 10
}
if timeoutSeconds == 0 {
timeoutSeconds = 10
}
exporter := &Exporter{
target: target,
timeout: timeout,
insecure: insecure,
timeout = time.Duration((timeoutSeconds) * 1e9)
}
ctx, cancel := context.WithTimeout(r.Context(), timeout)
defer cancel()
target := module.Target
if target == "" {
target = r.URL.Query().Get("target")
if target == "" {
http.Error(w, "Target parameter is missing", http.StatusBadRequest)
return
}
}
probeFunc, ok := prober.Probers[module.Prober]
if !ok {
http.Error(w, fmt.Sprintf("Unknown prober %q", module.Prober), http.StatusBadRequest)
return
}
var (
probeSuccess = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: prometheus.BuildFQName(namespace, "", "probe_success"),
Help: "If the probe was a success",
},
)
proberType = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: prometheus.BuildFQName(namespace, "", "prober"),
Help: "The prober used by the exporter to connect to the target",
},
[]string{"prober"},
)
)
registry := prometheus.NewRegistry()
registry.MustRegister(exporter)
registry.MustRegister(probeSuccess, proberType)
proberType.WithLabelValues(module.Prober).Set(1)
logger = log.With(logger, "target", target, "prober", module.Prober, "timeout", timeout)
err := probeFunc(ctx, logger, target, module, registry)
if err != nil {
level.Error(logger).Log("msg", err)
probeSuccess.Set(0)
} else {
probeSuccess.Set(1)
}
// Serve
h := promhttp.HandlerFor(registry, promhttp.HandlerOpts{})
h.ServeHTTP(w, r)
}
func uniq(certs []*x509.Certificate) []*x509.Certificate {
r := []*x509.Certificate{}
for _, c := range certs {
if !contains(r, c) {
r = append(r, c)
}
}
return r
}
func contains(certs []*x509.Certificate, cert *x509.Certificate) bool {
for _, c := range certs {
if (c.SerialNumber.String() == cert.SerialNumber.String()) && (c.Issuer.CommonName == cert.Issuer.CommonName) {
return true
}
}
return false
}
func init() {
prometheus.MustRegister(version.NewCollector(namespace + "_exporter"))
prometheus.MustRegister(versioncollector.NewCollector(namespace + "_exporter"))
}
func main() {
@@ -229,23 +124,36 @@ func main() {
listenAddress = kingpin.Flag("web.listen-address", "Address to listen on for web interface and telemetry.").Default(":9219").String()
metricsPath = kingpin.Flag("web.metrics-path", "Path under which to expose metrics").Default("/metrics").String()
probePath = kingpin.Flag("web.probe-path", "Path under which to expose the probe endpoint").Default("/probe").String()
insecure = kingpin.Flag("tls.insecure", "Skip certificate verification").Default("false").Bool()
configFile = kingpin.Flag("config.file", "SSL exporter configuration file").Default("").String()
promlogConfig = promlog.Config{}
err error
)
log.AddFlags(kingpin.CommandLine)
promlogflag.AddFlags(kingpin.CommandLine, &promlogConfig)
kingpin.Version(version.Print(namespace + "_exporter"))
kingpin.HelpFlag.Short('h')
kingpin.Parse()
log.Infoln("Starting "+namespace+"_exporter", version.Info())
log.Infoln("Build context", version.BuildContext())
logger := promlog.New(&promlogConfig)
http.Handle(*metricsPath, prometheus.Handler())
conf := config.DefaultConfig
if *configFile != "" {
conf, err = config.LoadConfig(*configFile)
if err != nil {
level.Error(logger).Log("msg", err)
os.Exit(1)
}
}
level.Info(logger).Log("msg", fmt.Sprintf("Starting %s_exporter %s", namespace, version.Info()))
level.Info(logger).Log("msg", fmt.Sprintf("Build context %s", version.BuildContext()))
http.Handle(*metricsPath, promhttp.Handler())
http.HandleFunc(*probePath, func(w http.ResponseWriter, r *http.Request) {
probeHandler(w, r, *insecure)
probeHandler(logger, w, r, conf)
})
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`<html>
_, _ = w.Write([]byte(`<html>
<head><title>SSL Exporter</title></head>
<body>
<h1>SSL Exporter</h1>
@@ -255,6 +163,7 @@ func main() {
</html>`))
})
log.Infoln("Listening on", *listenAddress)
log.Fatal(http.ListenAndServe(*listenAddress, nil))
level.Info(logger).Log("msg", fmt.Sprintf("Listening on %s", *listenAddress))
level.Error(logger).Log("msg", http.ListenAndServe(*listenAddress, nil))
os.Exit(1)
}

222
ssl_exporter_test.go Normal file
View File

@@ -0,0 +1,222 @@
package main
import (
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
"github.com/go-kit/log"
"github.com/ribbybibby/ssl_exporter/v2/config"
"github.com/ribbybibby/ssl_exporter/v2/test"
)
// TestProbeHandler tests that the probe handler sets the ssl_probe_success and
// ssl_prober metrics correctly
func TestProbeHandler(t *testing.T) {
server, _, _, caFile, teardown, err := test.SetupHTTPSServer()
if err != nil {
t.Fatalf(err.Error())
}
defer teardown()
server.StartTLS()
defer server.Close()
conf := &config.Config{
Modules: map[string]config.Module{
"https": config.Module{
Prober: "https",
TLSConfig: config.TLSConfig{
CAFile: caFile,
},
},
},
}
rr, err := probe(server.URL, "https", conf)
if err != nil {
t.Fatalf(err.Error())
}
// Check probe success
if ok := strings.Contains(rr.Body.String(), "ssl_probe_success 1"); !ok {
t.Errorf("expected `ssl_probe_success 1`")
}
// Check prober metric
if ok := strings.Contains(rr.Body.String(), "ssl_prober{prober=\"https\"} 1"); !ok {
t.Errorf("expected `ssl_prober{prober=\"https\"} 1`")
}
}
// TestProbeHandlerFail tests that the probe handler sets the ssl_probe_success and
// ssl_prober metrics correctly when the probe fails
func TestProbeHandlerFail(t *testing.T) {
rr, err := probe("localhost:6666", "", config.DefaultConfig)
if err != nil {
t.Fatalf(err.Error())
}
// Check probe success
if ok := strings.Contains(rr.Body.String(), "ssl_probe_success 0"); !ok {
t.Errorf("expected `ssl_probe_success 0`")
}
// Check prober metric
if ok := strings.Contains(rr.Body.String(), "ssl_prober{prober=\"tcp\"} 1"); !ok {
t.Errorf("expected `ssl_prober{prober=\"tcp\"} 1`")
}
}
// TestProbeHandlerDefaultModule tests the default module is used correctly
func TestProbeHandlerDefaultModule(t *testing.T) {
server, _, _, caFile, teardown, err := test.SetupHTTPSServer()
if err != nil {
t.Fatalf(err.Error())
}
defer teardown()
server.StartTLS()
defer server.Close()
conf := &config.Config{
DefaultModule: "https",
Modules: map[string]config.Module{
"tcp": config.Module{
Prober: "tcp",
TLSConfig: config.TLSConfig{
CAFile: caFile,
},
},
"https": config.Module{
Prober: "https",
TLSConfig: config.TLSConfig{
CAFile: caFile,
},
},
},
}
rr, err := probe(server.URL, "", conf)
if err != nil {
t.Fatalf(err.Error())
}
// Should have used the https prober
if ok := strings.Contains(rr.Body.String(), "ssl_prober{prober=\"https\"} 1"); !ok {
t.Errorf("expected `ssl_prober{prober=\"https\"} 1`")
}
conf.DefaultModule = ""
rr, err = probe(server.URL, "", conf)
if err != nil {
t.Fatalf(err.Error())
}
// It should fail when there's no default module
if rr.Code != http.StatusBadRequest {
t.Errorf("expected code: %d, got: %d", http.StatusBadRequest, rr.Code)
}
}
// TestProbeHandlerTarget tests the target module parameter is used correctly
func TestProbeHandlerDefaultTarget(t *testing.T) {
server, _, _, caFile, teardown, err := test.SetupHTTPSServer()
if err != nil {
t.Fatalf(err.Error())
}
defer teardown()
server.StartTLS()
defer server.Close()
conf := &config.Config{
Modules: map[string]config.Module{
"https": config.Module{
Prober: "https",
Target: server.URL,
TLSConfig: config.TLSConfig{
CAFile: caFile,
},
},
},
}
// Should use the target in the module configuration
rr, err := probe("", "https", conf)
if err != nil {
t.Fatalf(err.Error())
}
// Check probe success
if ok := strings.Contains(rr.Body.String(), "ssl_probe_success 1"); !ok {
t.Errorf("expected `ssl_probe_success 1`")
}
// Check prober metric
if ok := strings.Contains(rr.Body.String(), "ssl_prober{prober=\"https\"} 1"); !ok {
t.Errorf("expected `ssl_prober{prober=\"https\"} 1`")
}
// Should ignore a different target in the target parameter
rr, err = probe("localhost:6666", "https", conf)
if err != nil {
t.Fatalf(err.Error())
}
// Check probe success
if ok := strings.Contains(rr.Body.String(), "ssl_probe_success 1"); !ok {
t.Errorf("expected `ssl_probe_success 1`")
}
// Check prober metric
if ok := strings.Contains(rr.Body.String(), "ssl_prober{prober=\"https\"} 1"); !ok {
t.Errorf("expected `ssl_prober{prober=\"https\"} 1`")
}
conf.Modules["tcp"] = config.Module{
Prober: "tcp",
TLSConfig: config.TLSConfig{
CAFile: caFile,
},
}
rr, err = probe("", "tcp", conf)
if err != nil {
t.Fatalf(err.Error())
}
// It should fail when there's no target in the module configuration or
// the query parameters
if rr.Code != http.StatusBadRequest {
t.Errorf("expected code: %d, got: %d", http.StatusBadRequest, rr.Code)
}
}
func probe(target, module string, conf *config.Config) (*httptest.ResponseRecorder, error) {
uri := "/probe?target=" + target
if module != "" {
uri = uri + "&module=" + module
}
req, err := http.NewRequest("GET", uri, nil)
if err != nil {
return nil, err
}
rr := httptest.NewRecorder()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
probeHandler(newTestLogger(), w, r, conf)
})
handler.ServeHTTP(rr, req)
return rr, nil
}
func newTestLogger() log.Logger {
return log.NewLogfmtLogger(log.NewSyncWriter(os.Stdout))
}

99
test/https.go Normal file
View File

@@ -0,0 +1,99 @@
package test
import (
"crypto/tls"
"fmt"
"io"
"net"
"net/http"
"net/http/httptest"
"os"
"time"
)
// SetupHTTPSServer sets up a server for testing with a generated cert and key
// pair
func SetupHTTPSServer() (*httptest.Server, []byte, []byte, string, func(), error) {
testcertPEM, testkeyPEM := GenerateTestCertificate(time.Now().AddDate(0, 0, 1))
server, caFile, teardown, err := SetupHTTPSServerWithCertAndKey(testcertPEM, testcertPEM, testkeyPEM)
if err != nil {
return nil, testcertPEM, testkeyPEM, caFile, teardown, err
}
return server, testcertPEM, testkeyPEM, caFile, teardown, nil
}
// SetupHTTPSServerWithCertAndKey sets up a server with a provided certs and key
func SetupHTTPSServerWithCertAndKey(caPEM, certPEM, keyPEM []byte) (*httptest.Server, string, func(), error) {
var teardown func()
caFile, err := WriteFile("certfile.pem", caPEM)
if err != nil {
return nil, caFile, teardown, err
}
teardown = func() {
os.Remove(caFile)
}
testCert, err := tls.X509KeyPair(certPEM, keyPEM)
if err != nil {
return nil, caFile, teardown, err
}
server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello world")
}))
server.TLS = &tls.Config{
Certificates: []tls.Certificate{testCert},
}
return server, caFile, teardown, nil
}
// SetupHTTPProxyServer sets up a proxy server
func SetupHTTPProxyServer() (*httptest.Server, error) {
server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodConnect {
destConn, err := net.DialTimeout("tcp", r.Host, 10*time.Second)
if err != nil {
http.Error(w, err.Error(), http.StatusServiceUnavailable)
return
}
w.WriteHeader(http.StatusOK)
hijacker, ok := w.(http.Hijacker)
if !ok {
http.Error(w, "Hijacking not supported", http.StatusInternalServerError)
return
}
clientConn, _, err := hijacker.Hijack()
if err != nil {
http.Error(w, err.Error(), http.StatusServiceUnavailable)
}
go func() {
defer destConn.Close()
defer clientConn.Close()
_, err := io.Copy(destConn, clientConn)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}()
go func() {
defer clientConn.Close()
defer destConn.Close()
_, err := io.Copy(clientConn, destConn)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}()
} else {
fmt.Fprintln(w, "Hello world")
}
}))
return server, nil
}

329
test/tcp.go Normal file
View File

@@ -0,0 +1,329 @@
package test
import (
"bytes"
"crypto/tls"
"fmt"
"io"
"net"
"os"
"time"
"github.com/go-kit/log"
"github.com/go-kit/log/level"
)
// TCPServer allows manipulation of the tls.Config before starting the listener
type TCPServer struct {
Listener net.Listener
TLS *tls.Config
stopCh chan struct{}
logger log.Logger
}
// StartTLS starts a listener that performs an immediate TLS handshake
func (t *TCPServer) StartTLS() {
go func() {
ln := tls.NewListener(t.Listener, t.TLS)
conn, err := ln.Accept()
if err != nil {
panic(fmt.Sprintf("Error accepting on socket: %s", err))
}
defer conn.Close()
// Immediately upgrade to TLS.
if err := conn.(*tls.Conn).Handshake(); err != nil {
level.Error(t.logger).Log("msg", err)
} else {
// Send some bytes before terminating the connection.
fmt.Fprintf(conn, "Hello World!\n")
}
t.stopCh <- struct{}{}
}()
}
// StartTLSWait starts a listener and waits for duration 'd' before performing
// the TLS handshake
func (t *TCPServer) StartTLSWait(d time.Duration) {
go func() {
ln := tls.NewListener(t.Listener, t.TLS)
conn, err := ln.Accept()
if err != nil {
panic(fmt.Sprintf("Error accepting on socket: %s", err))
}
defer conn.Close()
time.Sleep(d)
if err := conn.(*tls.Conn).Handshake(); err != nil {
level.Error(t.logger).Log(err)
} else {
// Send some bytes before terminating the connection.
fmt.Fprintf(conn, "Hello World!\n")
}
t.stopCh <- struct{}{}
}()
}
// StartSMTP starts a listener that negotiates a TLS connection with an smtp
// client using STARTTLS
func (t *TCPServer) StartSMTP() {
go func() {
conn, err := t.Listener.Accept()
if err != nil {
panic(fmt.Sprintf("Error accepting on socket: %s", err))
}
defer conn.Close()
if err := conn.SetDeadline(time.Now().Add(5 * time.Second)); err != nil {
panic("Error setting deadline")
}
fmt.Fprintf(conn, "220 ESMTP StartTLS pseudo-server\n")
if _, e := fmt.Fscanf(conn, "EHLO prober\n"); e != nil {
panic("Error in dialog. No EHLO received.")
}
fmt.Fprintf(conn, "250-pseudo-server.example.net\n")
fmt.Fprintf(conn, "250-STARTTLS\n")
fmt.Fprintf(conn, "250 DSN\n")
if _, e := fmt.Fscanf(conn, "STARTTLS\n"); e != nil {
panic("Error in dialog. No (TLS) STARTTLS received.")
}
fmt.Fprintf(conn, "220 2.0.0 Ready to start TLS\n")
// Upgrade to TLS.
tlsConn := tls.Server(conn, t.TLS)
if err := tlsConn.Handshake(); err != nil {
level.Error(t.logger).Log("msg", err)
}
defer tlsConn.Close()
t.stopCh <- struct{}{}
}()
}
// StartSMTPWithDashInResponse starts a listener that negotiates a TLS connection with an smtp
// client using STARTTLS. The server provides the STARTTLS response in the form '250 STARTTLS'
// (with a space, rather than a dash)
func (t *TCPServer) StartSMTPWithDashInResponse() {
go func() {
conn, err := t.Listener.Accept()
if err != nil {
panic(fmt.Sprintf("Error accepting on socket: %s", err))
}
defer conn.Close()
if err := conn.SetDeadline(time.Now().Add(5 * time.Second)); err != nil {
panic("Error setting deadline")
}
fmt.Fprintf(conn, "220 ESMTP StartTLS pseudo-server\n")
if _, e := fmt.Fscanf(conn, "EHLO prober\n"); e != nil {
panic("Error in dialog. No EHLO received.")
}
fmt.Fprintf(conn, "250-pseudo-server.example.net\n")
fmt.Fprintf(conn, "250-DSN\n")
fmt.Fprintf(conn, "250 STARTTLS\n")
if _, e := fmt.Fscanf(conn, "STARTTLS\n"); e != nil {
panic("Error in dialog. No (TLS) STARTTLS received.")
}
fmt.Fprintf(conn, "220 2.0.0 Ready to start TLS\n")
// Upgrade to TLS.
tlsConn := tls.Server(conn, t.TLS)
if err := tlsConn.Handshake(); err != nil {
level.Error(t.logger).Log("msg", err)
}
defer tlsConn.Close()
t.stopCh <- struct{}{}
}()
}
// StartFTP starts a listener that negotiates a TLS connection with an ftp
// client using AUTH TLS
func (t *TCPServer) StartFTP() {
go func() {
conn, err := t.Listener.Accept()
if err != nil {
panic(fmt.Sprintf("Error accepting on socket: %s", err))
}
defer conn.Close()
fmt.Fprintf(conn, "220 Test FTP Service\n")
if _, e := fmt.Fscanf(conn, "AUTH TLS\n"); e != nil {
panic("Error in dialog. No AUTH TLS received.")
}
fmt.Fprintf(conn, "234 AUTH command ok. Expecting TLS Negotiation.\n")
// Upgrade to TLS.
tlsConn := tls.Server(conn, t.TLS)
if err := tlsConn.Handshake(); err != nil {
level.Error(t.logger).Log(err)
}
defer tlsConn.Close()
t.stopCh <- struct{}{}
}()
}
// StartIMAP starts a listener that negotiates a TLS connection with an imap
// client using STARTTLS
func (t *TCPServer) StartIMAP() {
go func() {
conn, err := t.Listener.Accept()
if err != nil {
panic(fmt.Sprintf("Error accepting on socket: %s", err))
}
defer conn.Close()
fmt.Fprintf(conn, "* OK XIMAP ready for requests\n")
if _, e := fmt.Fscanf(conn, ". CAPABILITY\n"); e != nil {
panic("Error in dialog. No . CAPABILITY received.")
}
fmt.Fprintf(conn, "* CAPABILITY IMAP4 IMAP4rev1 AUTH=PLAIN STARTTLS\n")
fmt.Fprintf(conn, ". OK CAPABILITY completed.\n")
if _, e := fmt.Fscanf(conn, ". STARTTLS\n"); e != nil {
panic("Error in dialog. No . STARTTLS received.")
}
fmt.Fprintf(conn, ". OK Begin TLS negotiation now.\n")
// Upgrade to TLS.
tlsConn := tls.Server(conn, t.TLS)
if err := tlsConn.Handshake(); err != nil {
level.Error(t.logger).Log("msg", err)
}
defer tlsConn.Close()
t.stopCh <- struct{}{}
}()
}
// StartPOP3 starts a listener that negotiates a TLS connection with an pop3
// client using STARTTLS
func (t *TCPServer) StartPOP3() {
go func() {
conn, err := t.Listener.Accept()
if err != nil {
panic(fmt.Sprintf("Error accepting on socket: %s", err))
}
defer conn.Close()
fmt.Fprintf(conn, "+OK XPOP3 ready.\n")
if _, e := fmt.Fscanf(conn, "STLS\n"); e != nil {
panic("Error in dialog. No STLS received.")
}
fmt.Fprintf(conn, "+OK Begin TLS negotiation now.\n")
// Upgrade to TLS.
tlsConn := tls.Server(conn, t.TLS)
if err := tlsConn.Handshake(); err != nil {
level.Error(t.logger).Log("msg", err)
}
defer tlsConn.Close()
t.stopCh <- struct{}{}
}()
}
// StartPostgreSQL starts a listener that negotiates a TLS connection with an postgresql
// client using STARTTLS
func (t *TCPServer) StartPostgreSQL() {
go func() {
conn, err := t.Listener.Accept()
if err != nil {
panic(fmt.Sprintf("Error accepting on socket: %s", err))
}
defer conn.Close()
sslRequestMessage := []byte{0x00, 0x00, 0x00, 0x08, 0x04, 0xd2, 0x16, 0x2f}
buffer := make([]byte, len(sslRequestMessage))
_, err = io.ReadFull(conn, buffer)
if err != nil {
panic("Error reading input from client")
}
if bytes.Compare(buffer, sslRequestMessage) != 0 {
panic(fmt.Sprintf("Error in dialog. No %x received", buffer))
}
sslRequestResponse := []byte{0x53}
if _, err := conn.Write(sslRequestResponse); err != nil {
panic("Error writing response to client")
}
tlsConn := tls.Server(conn, t.TLS)
if err := tlsConn.Handshake(); err != nil {
level.Error(t.logger).Log("msg", err)
}
defer tlsConn.Close()
t.stopCh <- struct{}{}
}()
}
// Close stops the server and closes the listener
func (t *TCPServer) Close() {
<-t.stopCh
t.Listener.Close()
}
// SetupTCPServer sets up a server for testing with a generated cert and key
// pair
func SetupTCPServer() (*TCPServer, []byte, []byte, string, func(), error) {
testcertPEM, testkeyPEM := GenerateTestCertificate(time.Now().AddDate(0, 0, 1))
server, caFile, teardown, err := SetupTCPServerWithCertAndKey(testcertPEM, testcertPEM, testkeyPEM)
if err != nil {
return nil, testcertPEM, testkeyPEM, caFile, teardown, err
}
return server, testcertPEM, testkeyPEM, caFile, teardown, nil
}
// SetupTCPServerWithCertAndKey sets up a server with the provided certs and key
func SetupTCPServerWithCertAndKey(caPEM, certPEM, keyPEM []byte) (*TCPServer, string, func(), error) {
var teardown func()
caFile, err := WriteFile("certfile.pem", caPEM)
if err != nil {
return nil, caFile, teardown, err
}
teardown = func() {
os.Remove(caFile)
}
testCert, err := tls.X509KeyPair(certPEM, keyPEM)
if err != nil {
return nil, caFile, teardown, err
}
tlsConfig := &tls.Config{
ServerName: "127.0.0.1",
Certificates: []tls.Certificate{testCert},
MinVersion: tls.VersionTLS13,
MaxVersion: tls.VersionTLS13,
}
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return nil, caFile, teardown, err
}
server := &TCPServer{
Listener: ln,
TLS: tlsConfig,
stopCh: make(chan (struct{})),
logger: log.NewLogfmtLogger(log.NewSyncWriter(os.Stdout)),
}
return server, caFile, teardown, err
}

105
test/test.go Normal file
View File

@@ -0,0 +1,105 @@
package test
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"io/ioutil"
"math/big"
"net"
"time"
)
// GenerateTestCertificate generates a test certificate with the given expiry date
func GenerateTestCertificate(expiry time.Time) ([]byte, []byte) {
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
panic(fmt.Sprintf("Error creating rsa key: %s", err))
}
pemKey := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)})
cert := GenerateCertificateTemplate(expiry)
cert.IsCA = true
_, pemCert := GenerateSelfSignedCertificateWithPrivateKey(cert, privateKey)
return pemCert, pemKey
}
// GenerateSignedCertificate generates a certificate that is signed
func GenerateSignedCertificate(cert, parentCert *x509.Certificate, parentKey *rsa.PrivateKey) (*x509.Certificate, []byte, []byte) {
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
panic(fmt.Sprintf("Error creating rsa key: %s", err))
}
derCert, err := x509.CreateCertificate(rand.Reader, cert, parentCert, &privateKey.PublicKey, parentKey)
if err != nil {
panic(fmt.Sprintf("Error signing test-certificate: %s", err))
}
genCert, err := x509.ParseCertificate(derCert)
if err != nil {
panic(fmt.Sprintf("Error parsing test-certificate: %s", err))
}
return genCert,
pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derCert}),
pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)})
}
// GenerateSelfSignedCertificateWithPrivateKey generates a self signed
// certificate with the given private key
func GenerateSelfSignedCertificateWithPrivateKey(cert *x509.Certificate, privateKey *rsa.PrivateKey) (*x509.Certificate, []byte) {
derCert, err := x509.CreateCertificate(rand.Reader, cert, cert, &privateKey.PublicKey, privateKey)
if err != nil {
panic(fmt.Sprintf("Error signing test-certificate: %s", err))
}
genCert, err := x509.ParseCertificate(derCert)
if err != nil {
panic(fmt.Sprintf("Error parsing test-certificate: %s", err))
}
return genCert, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derCert})
}
// GenerateCertificateTemplate generates the template used to issue test certificates
func GenerateCertificateTemplate(expiry time.Time) *x509.Certificate {
return &x509.Certificate{
BasicConstraintsValid: true,
SubjectKeyId: []byte{1},
SerialNumber: big.NewInt(100),
NotBefore: time.Now(),
NotAfter: expiry,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
IPAddresses: []net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("::1")},
Subject: pkix.Name{
CommonName: "example.ribbybibby.me",
Organization: []string{"ribbybibby"},
OrganizationalUnit: []string{"ribbybibbys org"},
},
EmailAddresses: []string{"me@ribbybibby.me", "example@ribbybibby.me"},
DNSNames: []string{"example.ribbybibby.me", "example-2.ribbybibby.me", "example-3.ribbybibby.me"},
}
}
// WriteFile writes some content to a temporary file
func WriteFile(filename string, contents []byte) (string, error) {
tmpFile, err := ioutil.TempFile("", filename)
if err != nil {
return tmpFile.Name(), err
}
if _, err := tmpFile.Write(contents); err != nil {
return tmpFile.Name(), err
}
if err := tmpFile.Close(); err != nil {
return tmpFile.Name(), err
}
return tmpFile.Name(), nil
}