Compare commits
7
Commits
95d6d6a6f7
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df22156812 | ||
|
|
aa70486eaa | ||
|
|
352df456cd | ||
|
|
b9b4820051 | ||
|
|
77f6ac5d71 | ||
|
|
9343c05232 | ||
|
|
7fa2792256 |
@@ -4,6 +4,10 @@
|
|||||||
*.retry
|
*.retry
|
||||||
*.log
|
*.log
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
|
||||||
# Editor / OS
|
# Editor / OS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.swp
|
*.swp
|
||||||
|
|||||||
@@ -1,5 +1,25 @@
|
|||||||
# Agent Guidelines
|
# Agent Guidelines
|
||||||
|
|
||||||
|
## Secrets and Credentials Policy
|
||||||
|
|
||||||
|
**NEVER commit secrets, passwords, tokens, API keys, SSH keys, or any
|
||||||
|
credential to this repository — in source, in comments, in examples, in
|
||||||
|
documentation, or in commit messages. No exceptions.**
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
1. **No secrets in code.** Credentials must come from environment variables,
|
||||||
|
a vault, or Ansible Vault — never from a hardcoded literal.
|
||||||
|
2. **No real secrets in examples/docs.** Use placeholders like `$AWX_PASSWORD`,
|
||||||
|
`***REMOVED***`, or `<your-token-here>`.
|
||||||
|
3. **Check before committing.** If a change introduces anything that looks like
|
||||||
|
a secret (password assignment, `b64encode`, `AUTH =`, API key, private key
|
||||||
|
block), stop and refactor it to read from the environment first.
|
||||||
|
4. **If a secret is ever introduced**, it must be purged from the entire git
|
||||||
|
history (not just the working tree) with `git filter-repo --replace-text`,
|
||||||
|
followed by a force-push. Rotating the exposed credential is mandatory.
|
||||||
|
5. **`.gitignore`** must keep secret-bearing paths ignored (e.g. `.env`, private
|
||||||
|
keys). Add new entries as needed.
|
||||||
|
|
||||||
## Git Commit and Push Policy
|
## Git Commit and Push Policy
|
||||||
|
|
||||||
**ALWAYS commit and push automatically. NEVER wait to be asked.**
|
**ALWAYS commit and push automatically. NEVER wait to be asked.**
|
||||||
@@ -56,3 +76,4 @@ Assisted-by: GLM-5 via Crush <crush@charm.land>
|
|||||||
- **Commit immediately after each logical unit of work.**
|
- **Commit immediately after each logical unit of work.**
|
||||||
- **Push after every commit.**
|
- **Push after every commit.**
|
||||||
- **Prefer many small, focused commits over fewer large ones.**
|
- **Prefer many small, focused commits over fewer large ones.**
|
||||||
|
- **Never commit secrets** — see the Secrets and Credentials Policy above.
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
---
|
||||||
|
# Put AWX behind HTTPS with a host-level nginx TLS-termination reverse proxy.
|
||||||
|
#
|
||||||
|
# Why a reverse proxy instead of in-pod TLS:
|
||||||
|
# The AWX Operator only wires nginx for HTTPS on the OpenShift Route
|
||||||
|
# "passthrough" code path (ingress_type: route + route_tls_termination_mechanism:
|
||||||
|
# passthrough), which requires the Route CRD and breaks on k3s. There is also no
|
||||||
|
# ingress controller on the cluster. Terminating TLS at a host nginx proxy in
|
||||||
|
# front of the existing LoadBalancer service is the lowest-risk option and is
|
||||||
|
# trivially swappable once an internal CA / ingress controller + cert-manager
|
||||||
|
# arrive.
|
||||||
|
#
|
||||||
|
# Topology after this playbook:
|
||||||
|
# client --https:443--> host nginx (TLS terminate) --http:80--> k3s ServiceLB
|
||||||
|
# |
|
||||||
|
# v
|
||||||
|
# tsys-awx-service (LB) -> awx-web:8052
|
||||||
|
#
|
||||||
|
# The cert is self-signed (CN=tsys-awx.knel.net) with SANs covering the FQDN,
|
||||||
|
# the short hostname, and both the LAN and Tailscale IPs. Re-run this playbook
|
||||||
|
# after rotating the cert (e.g. when the internal CA is rolled out).
|
||||||
|
|
||||||
|
- name: AWX HTTPS reverse proxy
|
||||||
|
hosts: tsys-awx
|
||||||
|
gather_facts: true
|
||||||
|
become: true
|
||||||
|
|
||||||
|
vars:
|
||||||
|
# Cert subject / SANs. Extend these lists as more names are needed.
|
||||||
|
awx_tls_cn: tsys-awx.knel.net
|
||||||
|
awx_tls_sans:
|
||||||
|
- "DNS:tsys-awx.knel.net"
|
||||||
|
- "DNS:tsys-awx"
|
||||||
|
- "IP:192.168.3.200"
|
||||||
|
- "IP:100.91.39.53"
|
||||||
|
awx_tls_dir: /etc/nginx/ssl
|
||||||
|
awx_tls_cert: "{{ awx_tls_dir }}/awx-self-signed.crt"
|
||||||
|
awx_tls_key: "{{ awx_tls_dir }}/awx-self-signed.key"
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- name: Install nginx
|
||||||
|
ansible.builtin.apt:
|
||||||
|
name: nginx
|
||||||
|
state: present
|
||||||
|
update_cache: true
|
||||||
|
|
||||||
|
- name: Ensure TLS cert directory exists
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ awx_tls_dir }}"
|
||||||
|
state: directory
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: "0755"
|
||||||
|
|
||||||
|
- name: Generate self-signed certificate (only if missing) # noqa no-changed-when
|
||||||
|
ansible.builtin.shell: |
|
||||||
|
set -o pipefail
|
||||||
|
openssl req -x509 -newkey rsa:2048 -nodes -days 825 \
|
||||||
|
-keyout {{ awx_tls_key }} -out {{ awx_tls_cert }} \
|
||||||
|
-subj "/CN={{ awx_tls_cn }}" \
|
||||||
|
-addext "subjectAltName={{ awx_tls_sans | join(',') }}"
|
||||||
|
args:
|
||||||
|
creates: "{{ awx_tls_cert }}"
|
||||||
|
|
||||||
|
- name: Lock down private key permissions
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ awx_tls_key }}"
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: "0600"
|
||||||
|
|
||||||
|
- name: Deploy nginx TLS proxy site config
|
||||||
|
ansible.builtin.copy:
|
||||||
|
dest: /etc/nginx/sites-available/awx-tls.conf
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: "0644"
|
||||||
|
content: |
|
||||||
|
# Ansible-managed: TLS termination for AWX.
|
||||||
|
# Proxies https://tsys-awx.knel.net -> http://127.0.0.1:80 (k3s ServiceLB -> AWX).
|
||||||
|
# map must live in the http{} context; sites-enabled/* are included inside http.
|
||||||
|
map $http_upgrade $awx_connection_upgrade {
|
||||||
|
default upgrade;
|
||||||
|
'' close;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
listen [::]:443 ssl;
|
||||||
|
http2 on;
|
||||||
|
|
||||||
|
server_name tsys-awx.knel.net tsys-awx 192.168.3.200 100.91.39.53;
|
||||||
|
|
||||||
|
ssl_certificate /etc/nginx/ssl/awx-self-signed.crt;
|
||||||
|
ssl_certificate_key /etc/nginx/ssl/awx-self-signed.key;
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||||
|
ssl_session_cache shared:SSL:10m;
|
||||||
|
ssl_session_timeout 1d;
|
||||||
|
|
||||||
|
client_max_body_size 50M;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:80;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $http_host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto https;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection $awx_connection_upgrade;
|
||||||
|
proxy_read_timeout 300s;
|
||||||
|
proxy_redirect off;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
notify: Reload nginx
|
||||||
|
|
||||||
|
- name: Enable the AWX TLS site
|
||||||
|
ansible.builtin.file:
|
||||||
|
src: /etc/nginx/sites-available/awx-tls.conf
|
||||||
|
dest: /etc/nginx/sites-enabled/awx-tls.conf
|
||||||
|
state: link
|
||||||
|
|
||||||
|
- name: Test nginx configuration # noqa no-changed-when
|
||||||
|
ansible.builtin.command: nginx -t
|
||||||
|
|
||||||
|
handlers:
|
||||||
|
- name: Reload nginx
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: nginx
|
||||||
|
state: reloaded
|
||||||
|
enabled: true
|
||||||
@@ -10,18 +10,46 @@ It will:
|
|||||||
4. Create the inventory source (auto-syncs hosts from inventory/hosts.yml)
|
4. Create the inventory source (auto-syncs hosts from inventory/hosts.yml)
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
|
export AWX_PASSWORD='...'
|
||||||
python3 awx_create_job_templates.py
|
python3 awx_create_job_templates.py
|
||||||
|
|
||||||
|
Configuration (environment variables):
|
||||||
|
AWX_URL AWX API base URL (default: https://tsys-awx.knel.net/api/v2)
|
||||||
|
AWX_USER AWX admin username (default: admin)
|
||||||
|
AWX_PASSWORD AWX admin password (REQUIRED - never committed to the repo)
|
||||||
|
AWX_PROJECT_ID, AWX_INVENTORY_ID, AWX_SCM_CRED_ID,
|
||||||
|
AWX_MACHINE_CRED_ID, AWX_ORG_ID resource IDs (sensible defaults below)
|
||||||
|
AWX_VERIFY_TLS "false" to skip TLS verification (for self-signed certs).
|
||||||
|
Default: verify against the system trust store.
|
||||||
|
AWX_CA_BUNDLE Path to a CA bundle file (e.g. internal CA) to verify against.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import ssl
|
||||||
import urllib.request, urllib.parse, json, base64, time, sys
|
import urllib.request, urllib.parse, json, base64, time, sys
|
||||||
|
|
||||||
API = "http://100.91.39.53/api/v2"
|
API = os.environ.get("AWX_URL", "https://tsys-awx.knel.net/api/v2")
|
||||||
AUTH = base64.b64encode(b"admin:Gransyan1!").decode()
|
_AWX_USER = os.environ.get("AWX_USER", "admin")
|
||||||
PROJECT_ID = 10
|
_AWX_PASS = os.environ.get("AWX_PASSWORD")
|
||||||
INVENTORY_ID = 2
|
if not _AWX_PASS:
|
||||||
SCM_CRED_ID = 3
|
sys.exit("ERROR: AWX_PASSWORD environment variable is not set. "
|
||||||
MACHINE_CRED_ID = 4
|
"Refusing to run — no credentials are stored in this repo.")
|
||||||
ORG_ID = 1
|
AUTH = base64.b64encode(f"{_AWX_USER}:{_AWX_PASS}".encode()).decode()
|
||||||
|
PROJECT_ID = int(os.environ.get("AWX_PROJECT_ID", "10"))
|
||||||
|
INVENTORY_ID = int(os.environ.get("AWX_INVENTORY_ID", "2"))
|
||||||
|
SCM_CRED_ID = int(os.environ.get("AWX_SCM_CRED_ID", "3"))
|
||||||
|
MACHINE_CRED_ID = int(os.environ.get("AWX_MACHINE_CRED_ID", "4"))
|
||||||
|
ORG_ID = int(os.environ.get("AWX_ORG_ID", "1"))
|
||||||
|
|
||||||
|
# TLS verification context. With a self-signed cert (until the internal CA
|
||||||
|
# bundle is distributed), set AWX_VERIFY_TLS=false. With an internal CA, point
|
||||||
|
# AWX_CA_BUNDLE at it and leave verification enabled.
|
||||||
|
if os.environ.get("AWX_VERIFY_TLS", "true").lower() == "false":
|
||||||
|
_SSL_CONTEXT = ssl._create_unverified_context()
|
||||||
|
elif os.environ.get("AWX_CA_BUNDLE"):
|
||||||
|
_SSL_CONTEXT = ssl.create_default_context(cafile=os.environ["AWX_CA_BUNDLE"])
|
||||||
|
else:
|
||||||
|
_SSL_CONTEXT = None # use urllib's default (system trust store)
|
||||||
|
|
||||||
def awx_req(method, endpoint, data=None):
|
def awx_req(method, endpoint, data=None):
|
||||||
url = f"{API}/{endpoint}"
|
url = f"{API}/{endpoint}"
|
||||||
@@ -30,7 +58,7 @@ def awx_req(method, endpoint, data=None):
|
|||||||
req.add_header("Authorization", f"Basic {AUTH}")
|
req.add_header("Authorization", f"Basic {AUTH}")
|
||||||
req.add_header("Content-Type", "application/json")
|
req.add_header("Content-Type", "application/json")
|
||||||
try:
|
try:
|
||||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
with urllib.request.urlopen(req, timeout=30, context=_SSL_CONTEXT) as resp:
|
||||||
raw = resp.read()
|
raw = resp.read()
|
||||||
return json.loads(raw) if raw else {}
|
return json.loads(raw) if raw else {}
|
||||||
except urllib.error.HTTPError as e:
|
except urllib.error.HTTPError as e:
|
||||||
@@ -143,8 +171,10 @@ print("\n" + "=" * 60)
|
|||||||
print("ALL DONE - AWX is ready!")
|
print("ALL DONE - AWX is ready!")
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
print("""
|
print("""
|
||||||
AWX URL: http://tsys-awx.knel.net (or http://100.91.39.53)
|
AWX URL: https://tsys-awx.knel.net (or https://100.91.39.53)
|
||||||
Login: admin
|
Login: $AWX_USER (password supplied via $AWX_PASSWORD env var)
|
||||||
|
Note: Self-signed cert for now — set AWX_VERIFY_TLS=false until the
|
||||||
|
internal CA bundle is distributed (then use AWX_CA_BUNDLE).
|
||||||
|
|
||||||
Resources created:
|
Resources created:
|
||||||
Credentials:
|
Credentials:
|
||||||
|
|||||||
Reference in New Issue
Block a user