├── .gitignore ├── README.md ├── buf.go ├── conn.go └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | pg_check_cert.* 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pg-check-cert: Simple command line utility that can be used to monitor postgresql certificates 2 | 3 | This script connects to a postgresql instance, checks the certificate and displays the amount of days left before it expires. 4 | It's intended to be used for monitoring your postgresql certificates, using a monitoring tool like [Zabbix](http://www.zabbix.com/) or [Nagios](https://www.nagios.org/). 5 | 6 | ## Why openssl is not enough 7 | I used to monitor my postgresql certificates using `openssl`. Unfortunately, the `openssl s_client` option does not support the postgresql handshake, and can therefore only look at the `.crt` file to monitor the expiration date. 8 | As postgresql needs to be restarted after the `.crt` file was replaced, the actual file might be updated, but postgresql is still using the old certificate in-memory, until the server is restarted (as of postgresql-9.5, `reload` is not sufficient to read in the new certificate). 9 | 10 | ## Installation 11 | Precompiled versions (linux-amd64, osx-amd64) are available on the [release page](https://github.com/chr4/pg-check-cert/releases). 12 | Download the file, extract it and move it to e.g. `/usr/local/bin`. 13 | 14 | ## Build 15 | ```shell 16 | go build -o pg-check-cert *.go 17 | ``` 18 | 19 | ## Usage 20 | ```shell 21 | pg-check-cert localhost:5432 22 | ``` 23 | 24 | ## Thanks 25 | - [thusoy/postgres-migm](https://github.com/thusoy/postgres-mitm/blob/master/postgres_get_server_cert.py) for the inspiration. 26 | - `buf.go` and `conn.go` are taken from [lib/pq](https://github.com/lib/pq/), see copyright notice in the respective files. 27 | -------------------------------------------------------------------------------- /buf.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011-2013, 'pq' Contributors Portions Copyright (C) 2011 Blake 3 | * Mizerany 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy 6 | * of this software and associated documentation files (the "Software"), to deal 7 | * in the Software without restriction, including without limitation the rights 8 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | * copies of the Software, and to permit persons to whom the Software is 10 | * furnished to do so, subject to the following conditions: 11 | * 12 | * The above copyright notice and this permission notice shall be included in 13 | * all copies or substantial portions of the Software. 14 | * 15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | * SOFTWARE. 22 | */ 23 | 24 | package main 25 | 26 | import ( 27 | "bytes" 28 | "encoding/binary" 29 | 30 | "github.com/lib/pq/oid" 31 | ) 32 | 33 | type readBuf []byte 34 | 35 | func (b *readBuf) int32() (n int) { 36 | n = int(int32(binary.BigEndian.Uint32(*b))) 37 | *b = (*b)[4:] 38 | return 39 | } 40 | 41 | func (b *readBuf) oid() (n oid.Oid) { 42 | n = oid.Oid(binary.BigEndian.Uint32(*b)) 43 | *b = (*b)[4:] 44 | return 45 | } 46 | 47 | // N.B: this is actually an unsigned 16-bit integer, unlike int32 48 | func (b *readBuf) int16() (n int) { 49 | n = int(binary.BigEndian.Uint16(*b)) 50 | *b = (*b)[2:] 51 | return 52 | } 53 | 54 | func (b *readBuf) string() string { 55 | i := bytes.IndexByte(*b, 0) 56 | if i < 0 { 57 | panic("invalid message format; expected string terminator") 58 | } 59 | s := (*b)[:i] 60 | *b = (*b)[i+1:] 61 | return string(s) 62 | } 63 | 64 | func (b *readBuf) next(n int) (v []byte) { 65 | v = (*b)[:n] 66 | *b = (*b)[n:] 67 | return 68 | } 69 | 70 | func (b *readBuf) byte() byte { 71 | return b.next(1)[0] 72 | } 73 | 74 | type writeBuf struct { 75 | buf []byte 76 | pos int 77 | } 78 | 79 | func (b *writeBuf) int32(n int) { 80 | x := make([]byte, 4) 81 | binary.BigEndian.PutUint32(x, uint32(n)) 82 | b.buf = append(b.buf, x...) 83 | } 84 | 85 | func (b *writeBuf) int16(n int) { 86 | x := make([]byte, 2) 87 | binary.BigEndian.PutUint16(x, uint16(n)) 88 | b.buf = append(b.buf, x...) 89 | } 90 | 91 | func (b *writeBuf) string(s string) { 92 | b.buf = append(b.buf, (s + "\000")...) 93 | } 94 | 95 | func (b *writeBuf) byte(c byte) { 96 | b.buf = append(b.buf, c) 97 | } 98 | 99 | func (b *writeBuf) bytes(v []byte) { 100 | b.buf = append(b.buf, v...) 101 | } 102 | 103 | func (b *writeBuf) wrap() []byte { 104 | p := b.buf[b.pos:] 105 | binary.BigEndian.PutUint32(p, uint32(len(p))) 106 | return b.buf 107 | } 108 | 109 | func (b *writeBuf) next(c byte) { 110 | p := b.buf[b.pos:] 111 | binary.BigEndian.PutUint32(p, uint32(len(p))) 112 | b.pos = len(b.buf) + 1 113 | b.buf = append(b.buf, c, 0, 0, 0, 0) 114 | } 115 | -------------------------------------------------------------------------------- /conn.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011-2013, 'pq' Contributors Portions Copyright (C) 2011 Blake 3 | * Mizerany 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy 6 | * of this software and associated documentation files (the "Software"), to deal 7 | * in the Software without restriction, including without limitation the rights 8 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | * copies of the Software, and to permit persons to whom the Software is 10 | * furnished to do so, subject to the following conditions: 11 | * 12 | * The above copyright notice and this permission notice shall be included in 13 | * all copies or substantial portions of the Software. 14 | * 15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | * SOFTWARE. 22 | */ 23 | 24 | package main 25 | 26 | import ( 27 | "bufio" 28 | "net" 29 | "time" 30 | ) 31 | 32 | type parameterStatus struct { 33 | // server version in the same format as server_version_num, or 0 if 34 | // unavailable 35 | serverVersion int 36 | 37 | // the current location based on the TimeZone value of the session, if 38 | // available 39 | currentLocation *time.Location 40 | } 41 | 42 | type transactionStatus byte 43 | 44 | const ( 45 | txnStatusIdle transactionStatus = 'I' 46 | txnStatusIdleInTransaction transactionStatus = 'T' 47 | txnStatusInFailedTransaction transactionStatus = 'E' 48 | ) 49 | 50 | type conn struct { 51 | c net.Conn 52 | buf *bufio.Reader 53 | namei int 54 | scratch [512]byte 55 | txnStatus transactionStatus 56 | 57 | parameterStatus parameterStatus 58 | 59 | saveMessageType byte 60 | saveMessageBuffer []byte 61 | 62 | // If true, this connection is bad and all public-facing functions should 63 | // return ErrBadConn. 64 | bad bool 65 | 66 | // If set, this connection should never use the binary format when 67 | // receiving query results from prepared statements. Only provided for 68 | // debugging. 69 | disablePreparedBinaryResult bool 70 | 71 | // Whether to always send []byte parameters over as binary. Enables single 72 | // round-trip mode for non-prepared Query calls. 73 | binaryParameters bool 74 | } 75 | 76 | func (c *conn) writeBuf(b byte) *writeBuf { 77 | c.scratch[0] = b 78 | return &writeBuf{ 79 | buf: c.scratch[:5], 80 | pos: 1, 81 | } 82 | } 83 | 84 | func (cn *conn) sendStartupPacket(m *writeBuf) { 85 | // sanity check 86 | if m.buf[0] != 0 { 87 | panic("oops") 88 | } 89 | 90 | _, err := cn.c.Write((m.wrap())[1:]) 91 | if err != nil { 92 | panic(err) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* pg_check_cert - Panic when your postgresql certificate is about to expire 2 | * Copyright (C) 2016 Chris Aumann 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package main 19 | 20 | import ( 21 | "crypto/tls" 22 | "fmt" 23 | "io" 24 | "net" 25 | "os" 26 | "time" 27 | ) 28 | 29 | func main() { 30 | if len(os.Args) != 2 { 31 | fmt.Printf("Usage: %s \n", os.Args[0]) 32 | os.Exit(1) 33 | } 34 | 35 | pgHost := os.Args[1] 36 | 37 | var err error 38 | cn := &conn{} 39 | 40 | // TODO: 41 | cn.c, err = net.Dial("tcp", pgHost) 42 | if err != nil { 43 | panic(err) 44 | } 45 | 46 | w := cn.writeBuf(0) 47 | w.int32(80877103) 48 | cn.sendStartupPacket(w) 49 | 50 | b := cn.scratch[:1] 51 | _, err = io.ReadFull(cn.c, b) 52 | if err != nil { 53 | panic(err) 54 | } 55 | 56 | if b[0] != 'S' { 57 | panic("SSL not supported") 58 | } 59 | 60 | tlsConf := tls.Config{} 61 | tlsConf.InsecureSkipVerify = true 62 | client := tls.Client(cn.c, &tlsConf) 63 | 64 | expiresIn := cn.expiresIn(client, &tlsConf) 65 | fmt.Println(expiresIn) 66 | } 67 | 68 | // Check how many days the certificate is still valid 69 | func (cn *conn) expiresIn(client *tls.Conn, tlsConf *tls.Config) int { 70 | err := client.Handshake() 71 | if err != nil { 72 | panic(err) 73 | } 74 | certs := client.ConnectionState().PeerCertificates 75 | 76 | expiresIn := certs[0].NotAfter.Sub(time.Now()) 77 | 78 | // Convert days to int 79 | return int(expiresIn.Hours() / 24) 80 | } 81 | --------------------------------------------------------------------------------