Skip to content

Commit f828113

Browse files
franckfermanclaude
andcommitted
Merge branch 'dev': document template system in README and docs site
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2 parents bc51f0e + ca00a44 commit f828113

2 files changed

Lines changed: 222 additions & 3 deletions

File tree

README.md

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
- [VPC](#vpc)
3939
- [Campaign](#campaign)
4040
- [Audit](#audit)
41+
- [Templates](#templates)
4142
- [JSON Output](#json-output)
4243
- [Shell Completion](#shell-completion)
4344
- [Library Usage](#library-usage)
@@ -98,6 +99,7 @@ Three tools are commonly used to manage DO infrastructure from a Red Team perspe
9899
| **VPC** | list, get, create, delete, members |
99100
| **Campaign** | deploy (multi-role), status, destroy, rotate-ips |
100101
| **Audit** | security scan with CRITICAL/HIGH/MEDIUM/INFO findings, CI-friendly exit codes |
102+
| **Templates** | list, show, dump + `--template` on droplet/campaign + `text/template` variable substitution |
101103

102104
Additional:
103105
- `-o json` on every command for scripting and pipelines
@@ -130,7 +132,8 @@ do-manager/
130132
│ ├── snapshot.go
131133
│ ├── vpc.go
132134
│ ├── campaign.go # campaign deploy/status/destroy/rotate-ips
133-
│ └── audit.go
135+
│ ├── audit.go
136+
│ └── template.go # template list/show/dump
134137
135138
├── internal/
136139
│ └── config/
@@ -145,7 +148,8 @@ do-manager/
145148
├── firewall/ # Firewalls + RuleSpec parser
146149
├── reservedip/
147150
├── snapshot/
148-
└── vpc/
151+
├── vpc/
152+
└── tmpl/ # embedded cloud-init templates + renderer
149153
```
150154

151155
`pkg/` is stable API surface. `cmd/` is CLI-only and not meant to be imported.
@@ -532,6 +536,62 @@ do-manager audit -o json | jq '.findings[] | select(.severity=="CRITICAL")'
532536

533537
---
534538

539+
### Templates
540+
541+
Built-in cloud-init bash scripts embedded in the binary. Variable substitution via Go's `text/template` (`{{.VarName}}` syntax). Unset variables fall back to declared defaults.
542+
543+
```bash
544+
# List all templates with their variables and defaults
545+
do-manager template list
546+
547+
# Inspect a template before deploying
548+
do-manager template show redirector-nginx
549+
550+
# Render a template with variable overrides and print to stdout (preview before deploy)
551+
do-manager template dump c2-havoc \
552+
--template-var C2Host=1.2.3.4 \
553+
--template-var TeamserverPassword=s3cr3t
554+
555+
# Use directly on droplet create
556+
do-manager droplet create --name c2-01 --region fra1 --image ubuntu-22-04-x64 \
557+
--template c2-havoc \
558+
--template-var C2Host=1.2.3.4 \
559+
--template-var TeamserverPassword=s3cr3t \
560+
--wait
561+
562+
# Use in multi-role campaign (template= token in role spec)
563+
do-manager campaign deploy \
564+
--name op-phantom --region fra1 --operator-ip 203.0.113.5 --vpc-auto \
565+
--role "c2:count=1:preset=c2:template=c2-havoc" \
566+
--role "redirector:count=3:preset=redirector:reserve-ips:template=redirector-nginx" \
567+
--template-var C2BackendHost=10.10.0.2 \
568+
--template-var Domain=cdn.example.com
569+
```
570+
571+
| Template | Description | Key variables |
572+
|---|---|---|
573+
| `c2-havoc` | Havoc C2 - build from source, teamserver as systemd service | `C2Port`, `C2Host`, `TeamserverPassword` |
574+
| `c2-sliver` | Sliver C2 - latest release, operator config, systemd service | `C2Port`, `OperatorName` |
575+
| `redirector-nginx` | nginx HTTPS reverse proxy, URI path filtering, certbot/self-signed TLS | `C2BackendHost`, `C2BackendPort`, `Domain`, `C2URIPath` |
576+
| `redirector-apache` | Apache2 mod_rewrite, User-Agent + URI filtering for C2 profiles | `C2BackendHost`, `C2UserAgent`, `C2URIPath` |
577+
| `wireguard-server` | WireGuard server - generates keys, configures wg0, enables IP forwarding | `WGListenPort`, `WGServerNet` |
578+
| `wireguard-client` | WireGuard peer - connects to a WireGuard server | `WGServerEndpoint`, `WGServerPublicKey`, `WGClientNet` |
579+
| `gophish` | GoPhish phishing framework - latest release, admin panel on localhost | `AdminListenPort`, `PhishListenPort` |
580+
| `evilginx2` | evilginx2 reverse proxy phishing - latest release, auto-detect public IP | `Domain`, `ExternalIP`, `RedirectURL` |
581+
| `hardening` | SSH hardening, fail2ban, non-root operator user | `SSHPort`, `OperatorUser`, `OperatorPubKey` |
582+
583+
Templates are stored in `pkg/tmpl/scripts/` and embedded in the binary via `//go:embed`. To add a custom template, add a `.sh` file with the metadata header:
584+
585+
```bash
586+
#!/bin/bash
587+
# @name: my-tool
588+
# @desc: One-line description shown in template list
589+
# @var: Port=443 - Listening port
590+
# @var: Password= - Admin password (required, no default)
591+
```
592+
593+
---
594+
535595
### JSON Output
536596

537597
Pass `-o json` to any command to get machine-readable output:
@@ -578,6 +638,7 @@ import (
578638
"github.com/franckferman/do-manager/pkg/firewall"
579639
"github.com/franckferman/do-manager/pkg/reservedip"
580640
"github.com/franckferman/do-manager/pkg/snapshot"
641+
"github.com/franckferman/do-manager/pkg/tmpl"
581642
)
582643

583644
func main() {
@@ -627,6 +688,14 @@ func main() {
627688
snaps, _ := snapSvc.List(ctx)
628689
snapSvc.CreateFromDroplet(ctx, d.ID, "my-snapshot", true) // true = wait
629690

691+
// Templates
692+
metas, _ := tmpl.List()
693+
script, _ := tmpl.Render("redirector-nginx", map[string]string{
694+
"C2BackendHost": "10.20.0.2",
695+
"Domain": "cdn.example.com",
696+
})
697+
_ = metas; _ = script
698+
630699
_ = list; _ = results; _ = action; _ = members; _ = fw; _ = snaps
631700
}
632701
```

docs/index.html

Lines changed: 151 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -775,6 +775,7 @@
775775
<a class="slink" href="#vpc" onclick="closeMobile()">VPC</a>
776776
<a class="slink" href="#campaign" onclick="closeMobile()">Campaign</a>
777777
<a class="slink" href="#fw-presets" onclick="closeMobile()">FW Presets</a>
778+
<a class="slink" href="#templates" onclick="closeMobile()">Templates</a>
778779
<a class="slink" href="#audit" onclick="closeMobile()">Audit</a>
779780
<a class="slink" href="#output-json" onclick="closeMobile()">JSON Output</a>
780781
<a class="slink" href="#completion" onclick="closeMobile()">Completion</a>
@@ -875,6 +876,7 @@ <h1 class="hero-title">
875876
<div class="sidebar-heading">Red Team</div>
876877
<a class="slink" href="#campaign">Campaign</a>
877878
<a class="slink sub" href="#fw-presets">Firewall Presets</a>
879+
<a class="slink" href="#templates">Templates</a>
878880
<a class="slink" href="#audit">Audit</a>
879881
</div>
880882
<div class="sidebar-section">
@@ -1958,6 +1960,147 @@ <h3>Rebuild a burned node</h3>
19581960

19591961
<hr />
19601962

1963+
<!-- TEMPLATES -->
1964+
<section id="templates" class="reveal">
1965+
<h2>Templates</h2>
1966+
<p>
1967+
Built-in cloud-init bash scripts embedded directly in the binary.
1968+
Each script installs and configures a specific tool (C2, redirector, VPN, phishing)
1969+
and is ready to run at Droplet first boot via DigitalOcean's cloud-init mechanism.
1970+
</p>
1971+
<p style="margin-top:.75rem">
1972+
Variable substitution uses Go's <code>text/template</code> (<code>&#123;&#123;.VarName&#125;&#125;</code> syntax).
1973+
Every variable has a declared default in the script header — unset variables silently
1974+
fall back to their defaults. Use <code>--template-var KEY=VALUE</code> to override.
1975+
</p>
1976+
1977+
<h3>Available templates</h3>
1978+
<div class="table-scroll">
1979+
<table class="flags-table">
1980+
<thead><tr><th>Name</th><th>Description</th><th>Key variables</th></tr></thead>
1981+
<tbody>
1982+
<tr>
1983+
<td><code>c2-havoc</code></td>
1984+
<td>Havoc C2 framework - build from source, teamserver as systemd service</td>
1985+
<td><code>C2Port</code> <code>C2Host</code> <code>TeamserverPassword</code></td>
1986+
</tr>
1987+
<tr>
1988+
<td><code>c2-sliver</code></td>
1989+
<td>Sliver C2 - download latest release, generate operator config, systemd</td>
1990+
<td><code>C2Port</code> <code>OperatorName</code></td>
1991+
</tr>
1992+
<tr>
1993+
<td><code>redirector-nginx</code></td>
1994+
<td>nginx HTTPS reverse proxy with URI path filtering. certbot or self-signed TLS.</td>
1995+
<td><code>C2BackendHost</code> <code>Domain</code> <code>C2URIPath</code></td>
1996+
</tr>
1997+
<tr>
1998+
<td><code>redirector-apache</code></td>
1999+
<td>Apache2 mod_rewrite with User-Agent + URI filtering for C2 profile matching</td>
2000+
<td><code>C2BackendHost</code> <code>C2UserAgent</code> <code>C2URIPath</code></td>
2001+
</tr>
2002+
<tr>
2003+
<td><code>wireguard-server</code></td>
2004+
<td>WireGuard VPN server — generates server keypair, configures wg0, enables IP forwarding</td>
2005+
<td><code>WGListenPort</code> <code>WGServerNet</code></td>
2006+
</tr>
2007+
<tr>
2008+
<td><code>wireguard-client</code></td>
2009+
<td>WireGuard peer — connects to a WireGuard server, prints client pubkey for server registration</td>
2010+
<td><code>WGServerEndpoint</code> <code>WGServerPublicKey</code> <code>WGClientNet</code></td>
2011+
</tr>
2012+
<tr>
2013+
<td><code>gophish</code></td>
2014+
<td>GoPhish phishing framework — latest release, admin panel bound to 127.0.0.1 only</td>
2015+
<td><code>AdminListenPort</code> <code>PhishListenPort</code></td>
2016+
</tr>
2017+
<tr>
2018+
<td><code>evilginx2</code></td>
2019+
<td>evilginx2 reverse proxy phishing — latest release, auto-detects public IP if not set</td>
2020+
<td><code>Domain</code> <code>ExternalIP</code> <code>RedirectURL</code></td>
2021+
</tr>
2022+
<tr>
2023+
<td><code>hardening</code></td>
2024+
<td>SSH hardening, fail2ban, non-root operator user creation with SSH key injection</td>
2025+
<td><code>SSHPort</code> <code>OperatorUser</code> <code>OperatorPubKey</code></td>
2026+
</tr>
2027+
</tbody>
2028+
</table>
2029+
</div>
2030+
2031+
<h3>Commands</h3>
2032+
<div class="terminal">
2033+
<div class="terminal-bar"><span class="tl tl-r"></span><span class="tl tl-y"></span><span class="tl tl-g"></span><span class="terminal-title">template list / show / dump</span><button class="copy-btn" onclick="copyTerminal(this)"><svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor"><path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 010 1.5h-1.5a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-1.5a.75.75 0 011.5 0v1.5A1.75 1.75 0 019.25 16h-7.5A1.75 1.75 0 010 14.25v-7.5z"/><path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0114.25 11h-7.5A1.75 1.75 0 015 9.25v-7.5zm1.75-.25a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-7.5a.25.25 0 00-.25-.25h-7.5z"/></svg>copy</button></div>
2034+
<div class="terminal-body">
2035+
<span class="dm"># List all templates with variables and defaults</span>
2036+
<span class="p">$ </span><span class="c">do-manager template list</span>
2037+
<span class="hd">&gt;&gt; 9 template(s)</span>
2038+
<span class="o"> NAME | DESCRIPTION | VARIABLES (DEFAULT)</span>
2039+
<span class="o"> c2-havoc | Havoc C2 framework ... | C2Port=443 C2Host TeamserverPassword=...</span>
2040+
<span class="o"> redirector-nginx | nginx HTTPS reverse proxy ... | C2BackendHost=10.20.0.2 Domain ...</span>
2041+
<span class="o"> wireguard-server | WireGuard VPN server ... | WGListenPort=51820 WGServerNet=...</span>
2042+
<span class="o"> ...</span>
2043+
<span class="dm"># Inspect raw script before deploying</span>
2044+
<span class="p">$ </span><span class="c">do-manager template show redirector-nginx</span>
2045+
<span class="dm"># Preview rendered output with your variable values</span>
2046+
<span class="p">$ </span><span class="c">do-manager template dump c2-havoc \</span>
2047+
<span class="o"> --template-var C2Host=1.2.3.4 \</span>
2048+
<span class="o"> --template-var TeamserverPassword=s3cr3t</span>
2049+
</div>
2050+
</div>
2051+
2052+
<h3>Deploy a single node with a template</h3>
2053+
<div class="terminal">
2054+
<div class="terminal-bar"><span class="tl tl-r"></span><span class="tl tl-y"></span><span class="tl tl-g"></span><span class="terminal-title">droplet create --template</span><button class="copy-btn" onclick="copyTerminal(this)"><svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor"><path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 010 1.5h-1.5a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-1.5a.75.75 0 011.5 0v1.5A1.75 1.75 0 019.25 16h-7.5A1.75 1.75 0 010 14.25v-7.5z"/><path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0114.25 11h-7.5A1.75 1.75 0 015 9.25v-7.5zm1.75-.25a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-7.5a.25.25 0 00-.25-.25h-7.5z"/></svg>copy</button></div>
2055+
<div class="terminal-body">
2056+
<span class="dm"># Spin up a GoPhish node in one command</span>
2057+
<span class="p">$ </span><span class="c">do-manager droplet create \</span>
2058+
<span class="o"> --name phish-01 --region fra1 --image ubuntu-22-04-x64 \</span>
2059+
<span class="o"> --template gophish \</span>
2060+
<span class="o"> --template-var AdminListenPort=3333 \</span>
2061+
<span class="o"> --wait</span>
2062+
<span class="ok">✓ phish-01 active 167.99.10.1</span>
2063+
<span class="dm"># Access the admin panel via SSH tunnel (admin is bound to 127.0.0.1 only)</span>
2064+
<span class="p">$ </span><span class="c">ssh -L 3333:127.0.0.1:3333 root@167.99.10.1</span>
2065+
<span class="dm"># Then open https://127.0.0.1:3333 in your browser</span>
2066+
</div>
2067+
</div>
2068+
2069+
<h3>Full campaign with templates per role</h3>
2070+
<div class="terminal">
2071+
<div class="terminal-bar"><span class="tl tl-r"></span><span class="tl tl-y"></span><span class="tl tl-g"></span><span class="terminal-title">campaign deploy with template= in role spec</span><button class="copy-btn" onclick="copyTerminal(this)"><svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor"><path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 010 1.5h-1.5a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-1.5a.75.75 0 011.5 0v1.5A1.75 1.75 0 019.25 16h-7.5A1.75 1.75 0 010 14.25v-7.5z"/><path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0114.25 11h-7.5A1.75 1.75 0 015 9.25v-7.5zm1.75-.25a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-7.5a.25.25 0 00-.25-.25h-7.5z"/></svg>copy</button></div>
2072+
<div class="terminal-body">
2073+
<span class="dm"># 1 Havoc C2 + 3 nginx redirectors, VPC isolated, fresh ubuntu images</span>
2074+
<span class="p">$ </span><span class="c">do-manager campaign deploy \</span>
2075+
<span class="o"> --name op-phantom \</span>
2076+
<span class="o"> --region fra1 \</span>
2077+
<span class="o"> --operator-ip 203.0.113.5 \</span>
2078+
<span class="o"> --vpc-auto \</span>
2079+
<span class="o"> --role "c2:count=1:image=ubuntu-22-04-x64:preset=c2:template=c2-havoc" \</span>
2080+
<span class="o"> --role "redirector:count=3:image=ubuntu-22-04-x64:preset=redirector:reserve-ips:template=redirector-nginx" \</span>
2081+
<span class="o"> --template-var C2BackendHost=10.10.0.2 \</span>
2082+
<span class="o"> --template-var Domain=cdn.example.com \</span>
2083+
<span class="o"> --template-var TeamserverPassword=s3cr3t</span>
2084+
<span class="ok">✓ VPC created: op-phantom-vpc 10.10.0.0/20</span>
2085+
<span class="ok">✓ Role c2: 1 Droplet(s) created Havoc installing at first boot</span>
2086+
<span class="ok">✓ Role redirector: 3 Droplet(s) created nginx installing at first boot ips=3</span>
2087+
<span class="dm"># --template-var values are shared across all roles</span>
2088+
<span class="dm"># C2BackendHost is used by redirector-nginx, TeamserverPassword by c2-havoc</span>
2089+
</div>
2090+
</div>
2091+
2092+
<div class="callout">
2093+
<strong>WireGuard workflow</strong><br><br>
2094+
WireGuard requires a two-step key exchange. Deploy the C2 with
2095+
<code>template=wireguard-server</code> first. The cloud-init script prints the server public key
2096+
in the boot logs (<code>journalctl -u cloud-init</code>). Then deploy the redirector with
2097+
<code>template=wireguard-client</code> and pass the server public key via
2098+
<code>--template-var WGServerPublicKey=&lt;key&gt;</code>.
2099+
</div>
2100+
</section>
2101+
2102+
<hr />
2103+
19612104
<!-- AUDIT -->
19622105
<section id="audit" class="reveal">
19632106
<h2>Audit</h2>
@@ -2025,6 +2168,7 @@ <h2>Library Usage</h2>
20252168
<span class="o"> </span><span class="ok">"github.com/franckferman/do-manager/pkg/snapshot"</span>
20262169
<span class="o"> </span><span class="ok">"github.com/franckferman/do-manager/pkg/sshkey"</span>
20272170
<span class="o"> </span><span class="ok">"github.com/franckferman/do-manager/pkg/region"</span>
2171+
<span class="o"> </span><span class="ok">"github.com/franckferman/do-manager/pkg/tmpl"</span>
20282172
<span class="o">)</span>
20292173
<span class="dm"></span>
20302174
<span class="fl">func</span><span class="o"> main() {</span>
@@ -2052,7 +2196,13 @@ <h2>Library Usage</h2>
20522196
<span class="o"> sizes, _ := reg.ListSizes(ctx)</span>
20532197
<span class="o"> images, _ := reg.ListImages(ctx, </span><span class="ok">"distribution"</span><span class="o">)</span>
20542198
<span class="dm"></span>
2055-
<span class="o"> _ = list; _ = d; _ = results; _ = keys; _ = regions; _ = sizes; _ = images</span>
2199+
<span class="dm"> // Templates</span>
2200+
<span class="o"> metas, _ := tmpl.List()</span>
2201+
<span class="o"> script, _ := tmpl.Render(</span><span class="vl">"redirector-nginx"</span><span class="o">, map[string]string{</span>
2202+
<span class="vl"> "C2BackendHost"</span><span class="o">: </span><span class="vl">"10.20.0.2"</span><span class="o">, </span><span class="vl">"Domain"</span><span class="o">: </span><span class="vl">"cdn.example.com"</span><span class="o">,</span>
2203+
<span class="o"> })</span>
2204+
<span class="dm"></span>
2205+
<span class="o"> _ = list; _ = d; _ = results; _ = keys; _ = regions; _ = sizes; _ = images; _ = metas; _ = script</span>
20562206
<span class="o">}</span>
20572207
</div>
20582208
</div>

0 commit comments

Comments
 (0)