├── docker-host-ipc
├── Dockerfile
├── namedpipe
└── README.md
├── auto-letsencrypt-cloudflare
├── Dockerfile
├── scripts
│ ├── cron
│ └── renew
└── README.md
├── setup
├── Dockerfile
├── README.md
└── setup
├── README.md
└── LICENSE
/docker-host-ipc/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM busybox
2 | COPY ./namedpipe /processor
3 | RUN chmod +x /processor
4 | CMD ["/processor"]
5 |
--------------------------------------------------------------------------------
/auto-letsencrypt-cloudflare/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM certbot/dns-cloudflare:latest
2 |
3 | ADD ./scripts /scripts
4 | RUN chmod +x /scripts/cron && chmod +x /scripts/renew
5 |
6 | VOLUME /ipc
7 |
8 | ENTRYPOINT ["/scripts/cron"]
9 |
--------------------------------------------------------------------------------
/setup/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM alpine:latest
2 | RUN apk update \
3 | && apk add git jq curl \
4 | && mkdir -p /app
5 |
6 | COPY setup /app/
7 | RUN chmod +x /app/setup
8 |
9 | VOLUME /output
10 |
11 | ENV USERNAME=""
12 | ENV TOKEN=""
13 |
14 | WORKDIR /app
15 |
16 | CMD ["/bin/sh", "/app/setup"]
--------------------------------------------------------------------------------
/auto-letsencrypt-cloudflare/scripts/cron:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | if [ -z "$CRON" ]; then
4 | CRON="0 */12 * * *"
5 | fi
6 |
7 | TMP_CRON_FILE=$(mktemp)
8 | echo "$CRON /scripts/renew" > "$TMP_CRON_FILE"
9 |
10 | crontab "$TMP_CRON_FILE"
11 |
12 | rm "$TMP_CRON_FILE"
13 |
14 | /scripts/renew
15 |
16 | crond -f
17 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Containers
2 |
3 | This repository contains purpose and task-specific containers (mostly Docker). Each directory contains a specific container configuration with its own README, so check the specific directory of the container for more documentation.
4 |
5 | 1. [**auto-letsencrypt-cloudflare**](./auto-letsencrypt-cloudflare): Automated LetsEncrypt SSL certificate creation/renewal with post-processing hooks.
6 | 2. **[docker-host-ipc](./docker-host-ipc)**: Communicate with the Docker host from within a Docker container.
7 | 2. **[setup](./setup)**: Acquire setup scripts from GitHub, with a one-liner.
8 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Author
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 all
13 | 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 |
--------------------------------------------------------------------------------
/docker-host-ipc/namedpipe:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | cleanup() {
3 | echo "Received SIGINT, closing..."
4 | exit 0
5 | }
6 |
7 | trap cleanup TERM INT
8 | set -e
9 |
10 | if [ -z "$NAMED_PIPE" ]; then
11 | echo "using default pipe /ipc/channel"
12 | NAMED_PIPE=/ipc/channel
13 | fi
14 |
15 | if [ ! -p "$NAMED_PIPE" ]; then
16 | echo "creating named pipe ($NAMED_PIPE)"
17 | DIR_PATH=$(dirname "$NAMED_PIPE")
18 | mkdir -p "$DIR_PATH"
19 | chmod 777 "$DIR_PATH"
20 | mkfifo "$NAMED_PIPE"
21 |
22 | if [ -z "$MAX_LOG_LINES" ]; then
23 | MAX_LOG_LINES=25000
24 | fi
25 | fi
26 |
27 | echo "now monitoring $NAMED_PIPE"
28 |
29 | mkdir -p /tmp
30 |
31 | while true; do
32 | if [ -n "$LOG" ]; then
33 | DIR_PATH=$(dirname "$LOG")
34 | mkdir -p "$DIR_PATH"
35 | touch "$LOG"
36 | result=$(eval "$(cat "$NAMED_PIPE")" 2>&1)
37 | echo "$(date '+%Y-%m-%d %H:%M:%S') $result" >> "$LOG"
38 | tail -n $MAX_LOG_LINES "$LOG" > /tmp/tmp.log && mv /tmp/tmp.log "$LOG"
39 | else
40 | eval "$(cat $NAMED_PIPE)"
41 | fi
42 | done
43 |
44 | # cleanup() {
45 | # echo "Received SIGINT, closing..."
46 | # exit 0
47 | # }
48 |
49 | # trap cleanup TERM INT
50 | # set -e
51 |
--------------------------------------------------------------------------------
/auto-letsencrypt-cloudflare/scripts/renew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # The domain to renew
3 | LIVE_PATH="/etc/letsencrypt/live/$DOMAIN"
4 | EMAIL=letsencrypt@$DOMAIN
5 |
6 | if [ -z "$DNS_PROPAGATION_SECONDS" ]; then
7 | DNS_PROPAGATION_SECONDS=10
8 | fi
9 |
10 | get_expire_time() {
11 | local cert_path="$1"
12 | local ds
13 |
14 | ds=$(openssl x509 -enddate -noout -in "$cert_path" | awk -F= '/notAfter/ {print $2}')
15 | date -u -D "%b %d %H:%M:%S %Y" -d "$(echo "$ds" | sed 's/GMT/UTC/')" '+%s'
16 | }
17 |
18 | get_create_time() {
19 | local cert_path="$1"
20 | local ds
21 |
22 | ds=$(openssl x509 -startdate -noout -in "$cert_path" | awk -F= '/notBefore/ {print $2}')
23 | date -u -D "%b %d %H:%M:%S %Y" -d "$(echo "$ds" | sed 's/GMT/UTC/')" '+%s'
24 | }
25 |
26 | # Check if the certificate exists
27 | if [ -L "$LIVE_PATH/cert.pem" ]; then
28 | # Get the expiration time of the certificate in seconds since epoch
29 | EXPIRATION_TIME=$(get_expire_time "$LIVE_PATH/cert.pem")
30 | CREATION_TIME=$(get_create_time "$LIVE_PATH/cert.pem")
31 |
32 | # Calculate the time until expiration in seconds
33 | TIME_UNTIL_EXPIRATION=$((EXPIRATION_TIME - CREATION_TIME))
34 |
35 | # Calculate the time until expiration in hours
36 | TIME_UNTIL_EXPIRATION_HOURS=$((TIME_UNTIL_EXPIRATION / 3600))
37 |
38 | # Check if the certificate will expire within the next 24 hours
39 | if [ "$TIME_UNTIL_EXPIRATION_HOURS" -ge 24 ]; then
40 | echo "$DOMAIN certificate renewal is unnecessary. It won't expire until $(openssl x509 -enddate -noout -in "$LIVE_PATH/cert.pem" | cut -d= -f2) ($TIME_UNTIL_EXPIRATION_HOURS hours)."
41 | if [ -f "$AFTER_ABORT" ]; then
42 | if [ -z "$IPC" ]; then
43 | cat "$AFTER_ABORT" > "$IPC"
44 | else
45 | chmod +x "$AFTER_ABORT"
46 | "$AFTER_ABORT" 2>&1
47 | fi
48 | else
49 | if [ ! -z "$AFTER_ABORT" ]; then
50 | if [ -z "$IPC" ]; then
51 | echo "$AFTER_ABORT" > "$IPC"
52 | else
53 | "$AFTER_ABORT" 2>&1
54 | fi
55 | fi
56 | fi
57 | exit 0
58 | fi
59 | fi
60 |
61 | # Generate if no cert exists or if an existing cert needs to be renewed within 24 hours.
62 | echo "Generating/renewing certificate for $DOMAIN: (takes minimum $DNS_PROPAGATION_SECONDS seconds)"
63 |
64 | # Create the Cloudflare credentials
65 | mkdir /secure
66 | if [ ! -f "/secure/cloudflare_credentials.ini" ]; then
67 | if [ -z "$CLOUDFLARE_API_TOKEN" ]; then
68 | echo "Cloudflare API token not found. Cannot create/renew $DOMAIN certificate"
69 | exit 1
70 | fi
71 |
72 | echo "dns_cloudflare_api_token = $CLOUDFLARE_API_TOKEN" > /secure/cloudflare_credentials.ini
73 | fi
74 |
75 | chmod 600 /secure/cloudflare_credentials.ini
76 |
77 | # Generate/renew certificate
78 | certbot certonly \
79 | --dns-cloudflare \
80 | --dns-cloudflare-credentials /secure/cloudflare_credentials.ini \
81 | --email $EMAIL \
82 | --register-unsafely-without-email \
83 | --agree-tos \
84 | --renew-by-default \
85 | --non-interactive \
86 | -d $DOMAIN
87 |
88 | # Check if the certificate exists
89 | if [[ -L "$LIVE_PATH/cert.pem" ]]; then
90 | echo "Validating $LIVE_PATH/cert.pem"
91 | # Get the expiration time of the certificate in seconds since epoch
92 | EXPIRATION_TIME=$(get_expire_time "$LIVE_PATH/cert.pem")
93 | CREATION_TIME=$(get_create_time "$LIVE_PATH/cert.pem")
94 |
95 | # Calculate the time until expiration in seconds
96 | TIME_UNTIL_EXPIRATION=$((EXPIRATION_TIME - CREATION_TIME))
97 |
98 | # Calculate the time until expiration in hours
99 | TIME_UNTIL_EXPIRATION_HOURS=$((TIME_UNTIL_EXPIRATION / 3600))
100 |
101 | # Check if the certificate will expire within the next 24 hours
102 | if [ "$TIME_UNTIL_EXPIRATION_HOURS" -ge 24 ]; then
103 | cat $LIVE_PATH/fullchain.pem $LIVE_PATH/privkey.pem > $LIVE_PATH/trusted.pem
104 | cat $LIVE_PATH/fullchain.pem > $LIVE_PATH/fullchain.crt
105 | cat $LIVE_PATH/chain.pem > $LIVE_PATH/ca.pem
106 | cat $LIVE_PATH/cert.pem > $LIVE_PATH/certificate.pem
107 | cat $LIVE_PATH/privkey.pem > $LIVE_PATH/private.key
108 |
109 | echo "$DOMAIN certificate renewal complete."
110 |
111 | if [ -f "$AFTER_SUCCESS" ]; then
112 | if [ -z "$IPC" ]; then
113 | cat "$AFTER_SUCCESS" > "$IPC"
114 | else
115 | chmod +x "$AFTER_SUCCESS"
116 | "$AFTER_SUCCESS" 2>&1
117 | fi
118 | else
119 | if [ ! -z "$AFTER_SUCCESS" ]; then
120 | if [ -z "$IPC" ]; then
121 | echo "$AFTER_SUCCESS" > "$IPC"
122 | else
123 | "$AFTER_SUCCESS" 2>&1
124 | fi
125 | fi
126 | fi
127 |
128 | exit 0
129 | fi
130 | fi
131 |
132 | echo "Could not find a recently created/updated $DOMAIN certificate at $LIVE_PATH"
133 | exit 1
134 |
--------------------------------------------------------------------------------
/docker-host-ipc/README.md:
--------------------------------------------------------------------------------
1 | # Docker Host IPC
2 |
3 | This Linux-only container establishes a named pipe that other Docker containers can use to communicate with the Docker host. This requires the use of the `--privileged` flag. Conceptually, this image configures a named pipe that accepts commands, runs them, and (optionally) logs stdout/stderr to a file.
4 |
5 | Why would you do this?
There are a few different ways for Docker containers to communicate with the host machine. Most are unnecessarily complex, such as using SSH to login to the host. This is a lot of overhead for communication that exists within a single server. It's also possible to setup a web server on the host to serve as a "bridge". That creates even more overhead than SSH communication.
It is far easier, with significantly less overhead/latency, to use a named pipe to communicate. It's not the kind of thing most developers remember how to do off the top of their head though. This image is designed for super-easy implementation while remaning as lightweight as lightest-weight as possible and restricting privileged use to an absolute minimum in a single container
6 |
7 | A named pipe is established on the Docker _host_, which can be mapped to other Docker containers that utilize the pipe for interprocess communications.
8 |
9 | **Run container...**
10 |
11 | ```sh
12 | docker run --rm \
13 | --restart always \
14 | --privileged \
15 | -e "NAMED_PIPE=/host_path/to/named/pipe" \
16 | -e "LOG=/host_path/to/pipe.log" \
17 | author/docker-host-ipc
18 | ```
19 |
20 | _or as part of docker compose_...
21 |
22 | ```sh
23 | version: '3'
24 | services:
25 | docker-ipc:
26 | image: author/docker-host-ipc
27 | privileged: true
28 | restart: always
29 | environment:
30 | NAMED_PIPE: "/host_path/to/named/pipe"
31 | LOG: "/host_path/to/pipe.log"
32 | ```
33 |
34 | A common pattern we've found to be effective is to create a directory on the host called `/ipc`, then run the docker container as follows:
35 |
36 | ```shell
37 | mkdir -p /ipc
38 | ```
39 |
40 | ```sh
41 | docker run --rm -d \
42 | --name ipc \
43 | --privileged \ # required
44 | -v "/ipc:/ipc" \ # required
45 | -e "NAMED_PIPE=/ipc/channel" \ # This line isn't technically necessary since the default is /ipc/channel. It is shown here to illustrate how the container works so you can choose a different named socket (i.e. other than /ipc/channel) if you prefer.
46 | -e "LOG=/ipc/channel.log" \ # Optional audit log
47 | author/docker-host-ipc
48 | ```
49 |
50 | This establishes a named pipe *on the host* at `/ipc/channel` and a log file available at `/ipc/channel.log`.
51 |
52 | **Environment Variables**
53 |
54 | | Variable | Required | Default | Description |
55 | | :---------------- | :------: | :--------------- | :------------------------------------------------------------------ |
56 | | `NAMED_PIPE` | No | `/ipc/channel` | The location of the named pipe. |
57 | | `LOG` | No | - | The location of the log file. |
58 | | `MAX_LOG_LINES` | No | `25000` | Truncate the log after this many lines (removes oldest lines first) |
59 |
60 | **Volumes**
61 |
62 | | Target | Required | Purpose |
63 | | :------- | :-----------: | :------------------------------------------------ |
64 | | `/ipc` | **Yes** | The_host_ directory where the named pipe resides. |
65 |
66 | ## Using the Named Pipe for IPC Within Docker Containers
67 |
68 | Other containers can utilize the pipe once it is created. Consider the following example Docker image:
69 |
70 | `Dockerfile` (built as `example`)
71 |
72 | ```sh
73 | FROM busybox
74 | VOLUME /ipc
75 | RUN echo "ls -l" > /ipc/channel
76 | ```
77 |
78 | This would be run as follows:
79 |
80 | ```sh
81 | docker run --rm -v "/ipc:/ipc" -e "LOG=/ipc/channel.log" example
82 | ```
83 |
84 | The result of `ls -l` (line 4 of Dockerfile) will be available in `/ipc/channel.log` on the host, which will look similar to:
85 |
86 | ```shell
87 | b
88 | ```
89 |
90 | Notice the logs are prefixed with the current timetamp.
91 |
92 | This is a trivial/contrived example, but it highlights how commands can be executed _on the Docker host_*from within a Docker container*.
93 |
94 | ## Motivation
95 |
96 | This image was initially created to support [auto-letsencrypt-cloudflare](../auto-letsencrypt-cloudflare/README.md). When SSL certificates are renewed, commands are issued to the Docker host to restart containers of the affected services. This prevents the need for other containers to have privileges, making this a more secure option for lightweight interprocess communication within a collection of Docker containers running on the same server.
97 |
98 | ---
99 |
100 | Copyright (c) 2024 Author Software, Inc. All rights reserved, subject to the MIT License.
101 |
--------------------------------------------------------------------------------
/setup/README.md:
--------------------------------------------------------------------------------
1 | # Setup Script
2 |
3 | This Docker image helps acquire content from Github repositories (both public and private). It provides a quick and secure way to retrieve infrastructure assets without having to configure git, SSH, FTP, or other utilities. The intention is to help rapidly bootstrap common infrastructure in a Docker environment with a short and memorable command.
4 |
5 | The command assumes a source repository contains setup scripts and assets in one of two arrangements: Directory-Based or Branch-Based.
6 |
7 | **Directory-Based Configurations**
8 | The target repository must be arranged with each top level directory representing a configuration.
9 |
10 | ```sh
11 | myorg/repo
12 | ├── discovery
13 | │ ├── docker-compose.yml
14 | │ └── setup.sh
15 | ├── ldap
16 | │ ├── docker-compose.yml
17 | │ └── setup.sh
18 | ├── web
19 | │ ├── docker-compose.yml
20 | │ └── setup.sh
21 | ├── api
22 | │ ├── docker-compose.yml
23 | │ └── setup.sh
24 | └── database
25 | ├── docker-compose.yml
26 | └── setup.sh
27 | ```
28 |
29 | **Branch-Based Configuration**
30 | In this arrangement, each branch represents a configuration. Source files are assumed to exist within the branch.
31 |
32 | ```sh
33 | myorg/repo branches
34 | ├── main
35 | ├── discovery
36 | ├── ldap
37 | ├── web
38 | ├── api
39 | └── database
40 | ```
41 |
42 | ## Memorable Command
43 |
44 | ```sh
45 | docker run --rm -it \
46 | -e "GITHUB_USER=github_username" \
47 | -e "GITHUB_TOKEN=github_personal_access_token" \
48 | -e "GITHUB_ORG=organization" \ # optional
49 | -v "./:/output" \
50 | author/setup
51 | ```
52 |
53 | This command has two required environment variables (Github user and token) and one optional environment variable (Github organization). The volume is also required. The mounted volume is where Github files are downloaded to on the host. In this example, we use the relative `./` directory, which will load the files into the current working directory.
54 |
55 | ### Usage
56 |
57 | This command automatically pulls the container from Docker Hub and runs a simple prompt wizard. If you specfiy a Github Organization, a list of repositories will be presented to choose from.
58 |
59 | ```sh
60 | Available repositories:
61 | 1. myorg/discovery
62 | 2. myorg/ldap
63 | 3. myorg/web
64 | 4. myorg/api
65 | 5. myorg/database
66 |
67 | Enter the number of the repo you wish to setup:
68 | ```
69 |
70 | If no organization is specified, you will be prompted to manually input the org/repository you wish to connect to.
71 |
72 | Next, the wizard prompts to determine whether you want to use the `Branch` or `Directory` method for retrieving content.
73 |
74 | ```sh
75 | Using author/infrastructure
76 |
77 | Setup from:
78 | 1. Branch
79 | 2. Directory
80 | q. Quit
81 |
82 | Enter the number of the repo arrangement:
83 | ```
84 |
85 | If `Branch` is selected, a prompt will appear to select the branch:
86 |
87 | ```sh
88 | Select a branch:
89 | 1. infrastructure-dev
90 | 2. infrastructure-prod
91 | 3. main
92 | q. Quit
93 |
94 | Enter the number of your choice:
95 | ```
96 |
97 | If `Directory` is selected, the top level directories of the repository will be listed, allowing selection of the directory to download.
98 |
99 | ```sh
100 | Retrieving myorg/containers top level directories...
101 |
102 | Select a directory or Quit:
103 | 1. auto-letsencrypt-cloudflare
104 | 2. docker-host-ipc
105 | q. Quit
106 |
107 | Enter the number of your choice:
108 | ```
109 |
110 | Once a branch or directory is selected, the wizard downloads the contents to the container's `/app` directory (which is mapped to the host container).
111 |
112 | When complete, the container will exit and remove itself.
113 |
114 | **Tip*
115 | If you wish to remove the image after the wizard is complete, add `&& docker rmi author/setup` to the end of the command. For example:
116 |
117 | ```sh
118 | docker run --rm -it \
119 | -e "GITHUB_USER=github_username" \
120 | -e "GITHUB_TOKEN=github_personal_access_token" \
121 | -e "GITHUB_ORG=organization" \ # optional
122 | -v "./:/output" \
123 | author/setup \
124 | && docker rmi author/setup
125 | ```
126 |
127 | We recommend doing this the last time you plan to run the command. The image is only relevant to setup and is unncessary once setup is complete.
128 |
129 | ### Next Steps
130 |
131 | It is assumed some sort of scripts are now available. In our practice, we create setup wizards for each type of infrastructure component we deploy. These are stored in a file called `setup`. This way, no matter which server we're configuring, there is a common file to setup/install what we need. Sometimes this script is as simple as `docker compose up -d`. Other times we have a wizard that generates a Dockerfile from a series of questions, or commands to install dependencies.
132 |
133 | As a result, we use the following command and attempt to commit it to memory:
134 |
135 | ```sh
136 | docker run --rm -it \
137 | -e "GITHUB_USER=github_username" \
138 | -e "GITHUB_TOKEN=github_personal_access_token" \
139 | -e "GITHUB_ORG=organization" \
140 | -v "./:/output" \
141 | author/setup \
142 | && docker rmi author/setup \
143 | && chmod +x ./setup \
144 | && ./setup
145 | ```
146 |
147 | This command downloads setup files, prepares permissions, and executes the `setup` file. As a result, administrators are greeted with a series of easy-to-answer prompts that automate the setup of our environments.
148 |
149 | ## Why?
150 |
151 | There are situations where the overhead of tools like Kubernetes (and even K3S) don't justify the extra effort. It can be easier to maintain configurations and updates in a single git repository as opposed to needing a database/consul/etcd. Bottom line: sometimes the simplest solution is just a series of scripts.
152 |
--------------------------------------------------------------------------------
/auto-letsencrypt-cloudflare/README.md:
--------------------------------------------------------------------------------
1 | # auto-letsencrypt-cloudflare
2 |
3 | This "always-on" Linux-only container keeps LetsEncrypt TLS/SSL certificates up to date. Post-create/renew hooks and physical file mirrors differentiate this container from other LetsEncrypt tools.
4 |
5 | ## Key Features:
6 |
7 | - **Always-on** (cron) runs the update process on a regular schedule (configurable).
8 | - **Hooks**: Optionally runs a user-defined script after successful certificate creation/renewal.
9 | - **Cloudflare DNS Verification**
10 | - **Physical Files**: For times when an application cannot read LetsEncrypt symlinks (see "Alternative to LetsEncrypt Symlinks" below).
11 |
12 | ### Example Use cases:
13 |
14 | - Auto-refresh LDAP certificates and reload the LDAP server upon completion.
15 | - Auto-renew Node.js certificates.
16 | - Auto-refresh a secure SMTP server.
17 | - Trigger webhooks when certificates are updated.
18 | - Send email notifications when certificates are updated.
19 |
20 | While it is possible to use this to refresh certificates for HTTP servers, there are many other dedicated tools better designed for that specific purpose (Caddy, NGINX, etc).
21 |
22 | ## Running in Docker
23 |
24 | **Environment Variables**
25 |
26 | | Name | Required | Default | Description |
27 | | :--------------------------------- | :-----------: | :--------------: | :------------------------------------------------------------------------------------------------------------- |
28 | | *`AFTER_SUCCESS` | No | - | The command or path of the script_on the host_ to run after the successful creation/renewal of certificates. |
29 | | `DNS_PROPAGATION_SECONDS` | No | `10` | Number of seconds to allow for DNS changes before LetsEncrypt verifies the DNS entries. |
30 | | **`DOMAIN`** | **Yes** | - | The domain to create/renew LetsEncrypt certificates for |
31 | | **`CLOUDFLARE_API_TOKEN`** | **Yes** | - | The Cloudflare API token. This token must permissions to create DNS entries. |
32 | | `CRON` | No | `0 */12 * * *` | The schedule to run the renewal process on. Default is every 12 hours. |
33 | | *`AFTER_ABORT` | No | - | The command or path of the script_on the host_ to run if a certificate is not created/renewed.
34 | | `IPC` | No | - | The IPC channel. |
35 |
36 | *Scripts reside on the host, not within the Docker container. They are executed via a [docker-host-ipc](../docker-host-ipc) channel between the container and the host.
37 |
38 | **Volumes**
39 |
40 | | Target | Purpose |
41 | | :--------- | :----------------------------------------------------- |
42 | | `/etc/letsencrypt` | Maps to the LetsEncrypt directory. **(REQUIRED)** |
43 | | `/ipc` | Maps to the named pipe. **(SEE NOTES)** |
44 |
45 | **Notes**
46 | If you wish to run scripts in response to creation/renewal (or lack thereof), you can run them in the container or on the host. Running within the container will isolate the script to the container. This is useful for running scripts that trigger webhooks, remote processes, etc. However; the script is limited to running in the confines of the container. There are circumstances where you may wish for the script to run on the Docker host instead of within the Docker container. There are two ways to do this.
47 |
48 | 1. Use the `--privileged` flag when running the container. This is a brute force approach that grants the container significant privileges (the same as the host kernel). It should be used with caution, in trusted environments only.
49 | 2. Use an IPC channel. This container will look for a named pipe at `/ipc` and pass commands to the named pipe, allowing them to run on the host. This requires additional configuration on the host. See the "IPC Communication" section for instructions.
50 |
51 | _Example:_
52 |
53 | ```sh
54 | docker run --rm \
55 | --restart unless-stopped \
56 | -e "DOMAIN=my.domain.com" \
57 | -e "CLOUDFLARE_API_TOKEN=mytoken" \
58 | -e "CRON=* 0/12 * * * *" \
59 | -e "AFTER_SUCCESS=/path/to/scriptname" \
60 | -e "AFTER_ABORT=/path/to/scriptname" \
61 | -e "IPC=/ipc/channel" \
62 | -v "/ipc:/ipc" \ # Maps the docker-host-ipc channel directory
63 | -v "/etc/letsencrypt:/etc/letsencrypt" \
64 | author/autorenew-letsencrypt-cloudflare
65 | ```
66 |
67 | _or as part of a docker-compose file..._
68 |
69 | ```sh
70 | version: '3'
71 | services:
72 | letsencrypt:
73 | image: author/autorenew-letsencrypt-cloudflare:latest
74 | container_name: mydomain_ssl_renewer
75 | environment: autorenew-letsencrypt-cloudflare
76 | DOMAIN: my.domain.com
77 | CLOUDFLARE_API_TOKEN: mytoken
78 | CRON: "0 */12 * * *"
79 | AFTER_SUCCESS: /handlers/scriptname
80 | AFTER_ABORT: /handlers/scriptname
81 | IPC: /ipc/channel
82 | volumes:
83 | /path/to/letsencrypt:/etc/letsencrypt
84 | /ipc:/ipc
85 | restart: unless-stopped
86 | ```
87 |
88 | ## Alernative to LetsEncrypt Symlinks
89 |
90 | LetsEncrypt generates pretty file names like `cert.pem` and `fullchain.pem`. However, these are symlinks to the most recently archived certificate assets, not true files. For example:
91 |
92 | `/etc/letsencrypt/live/my.domain.com/cert.pem -> /etc/letsencrypt/archive/my.domain.com/cert1.pem`
93 |
94 | Some applications cannot reliably read symlinks. Using the archive file directly poses a problem because the filename changes every time the certificate is renewed. This container makes a copy of the assets as physical files. Each time the certificate is renewed, the copy is recreated, assuring the same file name is available for each physical file. This avoids.
95 |
96 | In addition to the standard LetsEncrypt symlinks, the following files are generated in the `/etc/letsencrypt/live/` directory:
97 |
98 | | Physcial File | LetsEncrypt Equivalent |
99 | | :------------------ | :--------------------------------------------------------- |
100 | | `trusted.pem` | `fullchain.pem` + `privkey.pem`(concatenated into one) |
101 | | `fullchain.crt` | `fullchain.pem` |
102 | | `ca.pem` | `chain.pem` |
103 | | `certificate.pem` | `cert.pem` |
104 | | `private.key` | `privkey.pem` |
105 |
106 | ## IPC Communication
107 |
108 | In some cases, you may want to run commands on the host after a certificate is created/renewed. For example, you may wish to use LetsEncrypt certificates to secure an LDAP server running in another container . When the certificates are renewed, you may need to restart the LDAP Docker container or execute a command using `docker exec` on the LDAP container. This can only be done from the Docker host, not within an unprivileged Docker container.
109 |
110 | This container supports a [docker-host-ipc](../docker-host-ipc) channel (`/ipc`) to allow the execution of scripts/commands after the creation/renewal of certificates completes (or abort). This channel can be mapped into the `/ipc` volume of this container, allowing post-creation/renewal commands/scripts to run on the host.
111 |
112 | ---
113 |
114 | Copyright (c) 2024 Author Software, Inc.
115 |
--------------------------------------------------------------------------------
/setup/setup:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | # Function to download the contents of a directory
4 | download_directory() {
5 | local repo=$1
6 | local directory=$2
7 |
8 | mkdir -p /app/tmp
9 | cd /app/tmp
10 | if [ -p ".git" ]; then
11 | rm -r .git
12 | fi
13 | git config --global init.defaultBranch main
14 | git init
15 | git remote add -f origin https://$GITHUB_USER:$GITHUB_TOKEN@github.com/$repo.git
16 | git config core.sparsecheckout true
17 | echo $directory/ >> .git/info/sparse-checkout
18 | git pull origin main
19 |
20 | mv -if /app/tmp/$directory/* /output
21 | rm -rf /app/tmp
22 |
23 | echo "Download completed. Contents are in /app"
24 | ls -l /output
25 | }
26 |
27 | # Function to download the contents of a branch
28 | download_branch() {
29 | local repo=$1
30 | local branch=$2
31 |
32 | mkdir -p /app/tmp
33 | cd /app/tmp
34 | if [ -p ".git" ]; then
35 | rm -r .git
36 | fi
37 | git config --global init.defaultBranch $branch
38 | git init
39 | git remote add -f origin https://$GITHUB_USER:$GITHUB_TOKEN@github.com/$repo.git
40 | git config core.sparsecheckout true
41 | set -x
42 | git pull origin $branch
43 |
44 | mv -if /app/tmp/$directory/* /output
45 | rm -rf /app/tmp
46 |
47 | echo "Download completed. Contents are in /app"
48 | ls -l /output
49 | }
50 |
51 | # Prompt user for GitHub repository details
52 | if [ ! -z "$GITHUB_ORG" ]; then
53 | API_URL="https://api.github.com/orgs/$GITHUB_ORG/repos"
54 | response=$(curl -s -H "Authorization: token $GITHUB_TOKEN" $API_URL)
55 |
56 | # Check if the response is an array and not empty
57 | if [[ "$(echo "$response" | jq 'length > 0')" == "true" ]]; then
58 | # Extract repository names from the response using jq
59 | repo_names=$(echo "$response" | jq -r ".[].name")
60 |
61 | # Display the list of repository names
62 | echo "Available repositories:"
63 | count=1
64 | for r in $repo_names; do
65 | echo " $count. $GITHUB_ORG/$r"
66 | count=$((count+1))
67 | done
68 | echo " q. Quit"
69 | echo ""
70 |
71 | while true; do
72 | read -p "Enter the number of the repo to setup: " choice
73 | case $choice in
74 | [1-9]*)
75 | if [ $choice -le $count ]; then
76 | selected=$(echo $repo_names | awk "{print \$"$choice"}")
77 | echo ""
78 | echo "Using $GITHUB_ORG/$selected"
79 | repo="$GITHUB_ORG/$selected"
80 | break
81 | else
82 | echo "Invalid choice. Please enter a valid number."
83 | fi
84 | ;;
85 | "q" | "Q")
86 | echo "setup aborted"
87 | break
88 | ;;
89 | *)
90 | echo "Invalid choice. Please enter a valid number."
91 | ;;
92 | esac
93 | done
94 | else
95 | echo "No repositories found in $GITHUB_ORG accessible by $GITHUB_USER."
96 | exit 0
97 | fi
98 | else
99 | read -p "Repository: " repo
100 | # Verify repo exists
101 | chk_repo_url="https://api.github.com/repos/$repo"
102 | response=$(curl -H "Authorization: token $GITHUB_TOKEN" -s -o /dev/null -w "%{http_code}" "$chk_repo_url")
103 | if [ "$response" -eq 404 ]; then
104 | echo "Repository '$repo' does not exist."
105 | exit 1
106 | fi
107 | fi
108 |
109 | # Prompt for setup type
110 | echo ""
111 | echo "Setup from:"
112 | echo " 1. Branch"
113 | echo " 2. Directory"
114 | echo " q. Quit"
115 | echo ""
116 | while true; do
117 | read -p "Enter the number of the repo arrangement: " choice
118 | case $choice in
119 | 1)
120 | # Fetch remote branch names
121 | branch_names=$(git ls-remote --heads "https://$GITHUB_USER:$GITHUB_TOKEN@github.com/$repo.git" | awk -F/ '{print $NF}')
122 |
123 | echo ""
124 | echo "Select a branch:"
125 | count=1
126 | for dir in $branch_names; do
127 | echo " $count. $dir"
128 | count=$((count+1))
129 | done
130 | echo " q. Quit"
131 | echo ""
132 |
133 | while true; do
134 | read -p "Enter the number of your choice: " choice
135 | case $choice in
136 | [1-9]*)
137 | if [ $choice -le $count ]; then
138 | selected=$(echo $branch_names | awk "{print \$"$choice"}")
139 | echo "You selected branch: $selected"
140 | download_branch $repo $selected
141 | exit 0
142 | else
143 | echo "Invalid choice. Please enter a valid number."
144 | fi
145 | ;;
146 | "q" | "Q")
147 | echo "setup aborted"
148 | break
149 | ;;
150 | *)
151 | echo "Invalid choice. Please enter a valid number."
152 | ;;
153 | esac
154 | done
155 | ;;
156 | 2)
157 | repo_url="https://api.github.com/repos/$repo/contents"
158 |
159 | # Use curl to make a request to the GitHub API
160 | echo "Retrieving $repo top level directories..."
161 | response=$(curl -s -u "$GITHUB_USER:$GITHUB_TOKEN" $repo_url)
162 |
163 | # Check if the request was successful
164 | if [ $? -ne 0 ]; then
165 | echo "Failed to retrieve repository contents. Please check your credentials and repository information."
166 | exit 1
167 | fi
168 |
169 | # Check if the response is an error
170 | # Check if the response is an array
171 | if [ "$(echo "$response" | jq 'type')" = "array" ]; then
172 | # Check if the array has any elements
173 | if [ "$(echo "$response" | jq 'length')" -gt 0 ]; then
174 | # Check if the first element of the array has a "message" key
175 | if [ "$(echo "$response" | jq -r '.[0] | has("message")')" = "true" ]; then
176 | # Extract and print the error message
177 | err_msg=$(echo "$response" | jq -r '.[0].message')
178 | echo "Error: $err_msg"
179 | exit 1
180 | fi
181 | fi
182 | fi
183 |
184 | # Extract directory names from the API response
185 | directories=$(echo $response | jq -r '.[] | select(.type == "dir") | .name')
186 |
187 | # Check if there are any directories
188 | if [ -z "$directories" ]; then
189 | echo "No directories found in the main branch."
190 | exit 1
191 | fi
192 |
193 | # Display the menu
194 | echo ""
195 | echo "Select a directory or Quit:"
196 | count=1
197 | for dir in $directories; do
198 | echo " $count. $dir"
199 | count=$((count+1))
200 | done
201 | echo " q. Quit"
202 | echo ""
203 |
204 | # Prompt the user to select a directory
205 | while true; do
206 | read -p "Enter the number of your choice: " choice
207 | case $choice in
208 | [1-9]*)
209 | if [ $choice -le $count ]; then
210 | selected_dir=$(echo $directories | awk "{print \$"$choice"}")
211 | echo "You selected directory: $selected_dir"
212 | download_directory $repo $selected_dir
213 | exit 0
214 | else
215 | echo "Invalid choice. Please enter a valid number."
216 | fi
217 | ;;
218 | "q" | "Q")
219 | echo "setup aborted"
220 | break
221 | ;;
222 | *)
223 | echo "Invalid choice. Please enter a valid number."
224 | ;;
225 | esac
226 | done
227 | ;;
228 | "q" | "Q")
229 | echo "setup aborted"
230 | exit 0
231 | ;;
232 | *)
233 | echo "Invalid choice. Please enter a valid number."
234 | esac
235 | done
236 |
--------------------------------------------------------------------------------